aipp 1.0.0 → 2.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -2
- data/CHANGELOG.md +17 -1
- data/README.md +269 -150
- data/exe/aip2aixm +2 -8
- data/exe/aip2ofmx +2 -8
- data/exe/notam2aixm +5 -0
- data/exe/notam2ofmx +5 -0
- data/lib/aipp/aip/README.md +10 -0
- data/lib/aipp/aip/executable.rb +40 -0
- data/lib/aipp/aip/parser.rb +9 -0
- data/lib/aipp/aip/runner.rb +85 -0
- data/lib/aipp/border.rb +2 -2
- data/lib/aipp/debugger.rb +14 -19
- data/lib/aipp/downloader/file.rb +57 -0
- data/lib/aipp/downloader/graphql.rb +29 -0
- data/lib/aipp/downloader/http.rb +48 -0
- data/lib/aipp/downloader.rb +78 -29
- data/lib/aipp/environment.rb +88 -0
- data/lib/aipp/executable.rb +36 -53
- data/lib/aipp/notam/README.md +25 -0
- data/lib/aipp/notam/executable.rb +27 -0
- data/lib/aipp/notam/parser.rb +9 -0
- data/lib/aipp/notam/runner.rb +28 -0
- data/lib/aipp/parser.rb +133 -160
- data/lib/aipp/patcher.rb +4 -5
- data/lib/aipp/regions/LF/README.md +6 -2
- data/lib/aipp/regions/LF/aip/aerodromes.rb +220 -0
- data/lib/aipp/regions/LF/aip/d_p_r_airspaces.rb +53 -0
- data/lib/aipp/regions/LF/aip/dangerous_activities.rb +48 -0
- data/lib/aipp/regions/LF/aip/designated_points.rb +44 -0
- data/lib/aipp/regions/LF/aip/helipads.rb +119 -0
- data/lib/aipp/regions/LF/aip/navigational_aids.rb +82 -0
- data/lib/aipp/regions/LF/aip/obstacles.rb +150 -0
- data/lib/aipp/regions/LF/aip/serviced_airspaces.rb +67 -0
- data/lib/aipp/regions/LF/aip/services.rb +169 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +2 -2
- data/lib/aipp/regions/LF/helpers/base.rb +32 -32
- data/lib/aipp/regions/LS/README.md +59 -0
- data/lib/aipp/regions/LS/helpers/base.rb +111 -0
- data/lib/aipp/regions/LS/notam/ENR.rb +173 -0
- data/lib/aipp/runner.rb +152 -0
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +30 -11
- data/lib/core_ext/array.rb +13 -0
- data/lib/core_ext/nokogiri.rb +56 -8
- data/lib/core_ext/string.rb +63 -1
- data.tar.gz.sig +0 -0
- metadata +115 -64
- metadata.gz.sig +0 -0
- data/lib/aipp/aip.rb +0 -166
- data/lib/aipp/regions/LF/aerodromes.rb +0 -223
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +0 -56
- data/lib/aipp/regions/LF/dangerous_activities.rb +0 -49
- data/lib/aipp/regions/LF/designated_points.rb +0 -47
- data/lib/aipp/regions/LF/helipads.rb +0 -122
- data/lib/aipp/regions/LF/navigational_aids.rb +0 -85
- data/lib/aipp/regions/LF/obstacles.rb +0 -153
- data/lib/aipp/regions/LF/serviced_airspaces.rb +0 -70
- data/lib/aipp/regions/LF/services.rb +0 -172
@@ -0,0 +1,111 @@
|
|
1
|
+
module AIPP
|
2
|
+
module NewayAPI
|
3
|
+
HttpAdapter = GraphQL::Client::HTTP.new(ENV['NEWAY_API_URL']) do
|
4
|
+
def headers(context)
|
5
|
+
{ "Authorization": "Bearer #{ENV['NEWAY_API_AUTHORIZATION']}" }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
Schema = GraphQL::Client.load_schema(HttpAdapter)
|
9
|
+
Client = GraphQL::Client.new(schema: Schema, execute: HttpAdapter)
|
10
|
+
|
11
|
+
class Notam
|
12
|
+
Query = Client.parse <<~END
|
13
|
+
query ($region: String!, $series: [String!], $start: Int!, $end: Int!) {
|
14
|
+
queryNOTAMs(
|
15
|
+
filter: {region: $region, series: $series, start: $start, end: $end}
|
16
|
+
) {
|
17
|
+
notamRaw
|
18
|
+
}
|
19
|
+
}
|
20
|
+
END
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module LS
|
25
|
+
module Helpers
|
26
|
+
module Base
|
27
|
+
|
28
|
+
using AIXM::Refinements
|
29
|
+
|
30
|
+
# Mandatory Interface
|
31
|
+
|
32
|
+
def setup
|
33
|
+
AIPP.cache.aip = read('AIP').css('Ase')
|
34
|
+
AIPP.cache.dabs = read('DABS')
|
35
|
+
end
|
36
|
+
|
37
|
+
def origin_for(document)
|
38
|
+
case document
|
39
|
+
when 'ENR'
|
40
|
+
variables = {
|
41
|
+
region: 'LS',
|
42
|
+
series: %w(W B),
|
43
|
+
start: aixm.effective_at.beginning_of_day.to_i,
|
44
|
+
end: aixm.expiration_at.to_i
|
45
|
+
}
|
46
|
+
verbose_info("Querying API with #{variables}")
|
47
|
+
AIPP::Downloader::GraphQL.new(
|
48
|
+
client: AIPP::NewayAPI::Client,
|
49
|
+
query: AIPP::NewayAPI::Notam::Query,
|
50
|
+
variables: variables
|
51
|
+
)
|
52
|
+
when 'AD'
|
53
|
+
fail "not yet implemented"
|
54
|
+
when 'AIP'
|
55
|
+
AIPP::Downloader::HTTP.new(
|
56
|
+
archive: "https://snapshots.openflightmaps.org/live/#{AIRAC::Cycle.new.id}/ofmx/lsas/latest/ofmx_ls.zip",
|
57
|
+
file: "ofmx_ls/isolated/ofmx_ls.ofmx"
|
58
|
+
)
|
59
|
+
when 'DABS'
|
60
|
+
if aixm.effective_at.to_date == Date.today # DABS cross check works reliably for today only
|
61
|
+
AIPP::Downloader::HTTP.new(
|
62
|
+
file: "https://www.skybriefing.com/o/dabs?today",
|
63
|
+
type: :pdf
|
64
|
+
)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
fail "document not recognized"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Templates
|
72
|
+
|
73
|
+
def organisation_lf
|
74
|
+
unless AIPP.cache.organisation_lf
|
75
|
+
AIPP.cache.organisation_lf = AIXM.organisation(
|
76
|
+
source: source(position: 1, document: "GEN-3.1"),
|
77
|
+
name: 'SWITZERLAND',
|
78
|
+
type: 'S'
|
79
|
+
).tap do |organisation|
|
80
|
+
organisation.id = 'LS'
|
81
|
+
end
|
82
|
+
add AIPP.cache.organisation_ls
|
83
|
+
end
|
84
|
+
AIPP.cache.organisation_ls
|
85
|
+
end
|
86
|
+
|
87
|
+
# Parserettes
|
88
|
+
|
89
|
+
def timetable_from(schedules)
|
90
|
+
AIXM.timetable.tap do |timetable|
|
91
|
+
schedules&.each do |schedule|
|
92
|
+
schedule.actives.each do |actives|
|
93
|
+
schedule.times.each do |times|
|
94
|
+
timesheet = AIXM.timesheet(
|
95
|
+
adjust_to_dst: false,
|
96
|
+
dates: (actives.instance_of?(Range) ? actives : (actives..actives))
|
97
|
+
# TODO: transform to...
|
98
|
+
# dates: actives
|
99
|
+
)
|
100
|
+
timesheet.times = times
|
101
|
+
timetable.add_timesheet timesheet
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
using AIXM::Refinements
|
2
|
+
|
3
|
+
module AIPP::LS::NOTAM
|
4
|
+
class ENR < AIPP::NOTAM::Parser
|
5
|
+
|
6
|
+
include AIPP::LS::Helpers::Base
|
7
|
+
|
8
|
+
def parse
|
9
|
+
json = read
|
10
|
+
fail "malformed JSON received from API" unless json.has_key?('queryNOTAMs')
|
11
|
+
added_notam_ids = []
|
12
|
+
json['queryNOTAMs'].each do |row|
|
13
|
+
next unless row['notamRaw'].match? /^Q\) LS/ # only parse national NOTAM
|
14
|
+
|
15
|
+
# HACK: try to add missing commas to D-item of A- and B-series NOTAM
|
16
|
+
if row['notamRaw'].match? /\A[AB]/
|
17
|
+
if row['notamRaw'].gsub!(/(#{NOTAM::Schedule::HOUR_RE.decapture}-#{NOTAM::Schedule::HOUR_RE.decapture})/, '\1,')
|
18
|
+
row['notamRaw'].gsub!(/,+/, ',')
|
19
|
+
row['notamRaw'].sub!(/,\n/, "\n")
|
20
|
+
warn("HACK: added missing commas to D item")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
(notam = notam_for(row['notamRaw'])) or next
|
25
|
+
if respect? notam
|
26
|
+
next if notam.data[:five_day_schedules] == []
|
27
|
+
added_notam_ids << notam.data[:id]
|
28
|
+
add(
|
29
|
+
case notam.data[:content]
|
30
|
+
when /\A[DR].AREA.+ACT/, /TMA.+ACT/
|
31
|
+
if fragment = fragment_for(notam)
|
32
|
+
AIXM.generic(fragment: fragment_for(notam)).tap do |airspace|
|
33
|
+
element = airspace.fragment.children.first
|
34
|
+
element.prepend_child(['<!--', notam.text ,'-->'].join("\n"))
|
35
|
+
content = ["NOTAM #{notam.data[:id]}", element.at_css('txtName').content].join(": ").strip
|
36
|
+
element.at_css('txtName').content = content
|
37
|
+
content = [element.at_css('txtRmk')&.text, notam.data[:translated_content]].join("\n").strip
|
38
|
+
element.find_or_add_child('txtRmk').content = content
|
39
|
+
if schedule = notam.data[:five_day_schedules]
|
40
|
+
timetable = timetable_from(schedule)
|
41
|
+
element
|
42
|
+
.find_or_add_child('Att', before_css: %w(codeSelAvbl txtRmk))
|
43
|
+
.replace(timetable.to_xml(as: :Att).chomp)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
warn "no feature found for `#{notam.data[:content]}' - fallback to point and radius"
|
48
|
+
airspace_from(notam).tap do |airspace|
|
49
|
+
airspace.geometry = geometry_from_q_item(notam)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
when /\ATEMPO [DR].AREA.+(?:ACT|EST|ESTABLISHED) WI AREA/
|
53
|
+
airspace_from(notam).tap do |airspace|
|
54
|
+
airspace.geometry = geometry_from_content(notam)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
airspace_from(notam).tap do |airspace|
|
58
|
+
airspace.geometry = geometry_from_q_item(notam)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
)
|
62
|
+
else
|
63
|
+
verbose_info("Skipping NOTAM #{notam.data[:id]}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
dabs_cross_check(added_notam_ids)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def notam_for(raw_notam)
|
72
|
+
notam_id = raw_notam.strip.split(/\s+/, 2).first
|
73
|
+
if AIPP.options.crossload
|
74
|
+
crossload_file = AIPP.options.crossload.join('LS', "#{notam_id.sub('/', '_')}.txt")
|
75
|
+
if File.exist? crossload_file
|
76
|
+
info("crossloading #{crossload_file}")
|
77
|
+
return NOTAM.parse(crossload_file.read)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
NOTAM.parse(raw_notam)
|
81
|
+
rescue
|
82
|
+
warn "cannot parse #{notam_id}"
|
83
|
+
raise if AIPP.options.force
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Boolean] whether to respect this NOTAM or ignore it
|
87
|
+
def respect?(notam)
|
88
|
+
notam.data[:condition] != :checklist && (
|
89
|
+
notam.data[:scope].include?(:navigation_warning) ||
|
90
|
+
%i(terminal_control_area).include?(notam.data[:subject]) # TODO: include :obstacle as well
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def fragment_for(notam)
|
95
|
+
case notam.data[:content]
|
96
|
+
when /(?<type>TMA) ((SECT )?(?<section>\d+) )?ACT/
|
97
|
+
'Ase:has(codeType:contains("%s") + codeId:contains("%s %s"))' % [$~['type'], notam.data[:locations].first, $~['section']]
|
98
|
+
when /[DR].AREA (?<name>LS-[DR]\d+[A-Z]?).+ACT/
|
99
|
+
'Ase:has(codeId:matches("^%s( .+)?$"))' % [$~['name']]
|
100
|
+
else
|
101
|
+
return
|
102
|
+
end.then do |selector|
|
103
|
+
AIPP.cache.aip.at_css(selector, Nokogiri::MATCHES)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def airspace_from(notam)
|
108
|
+
AIXM.airspace(
|
109
|
+
id: notam.data[:id],
|
110
|
+
type: :regulated_airspace,
|
111
|
+
name: "NOTAM #{notam.data[:id]}"
|
112
|
+
).tap do |airspace|
|
113
|
+
airspace.add_layer(
|
114
|
+
AIXM.layer(
|
115
|
+
vertical_limit: AIXM.vertical_limit(
|
116
|
+
upper_z: notam.data[:upper_limit],
|
117
|
+
lower_z: notam.data[:lower_limit]
|
118
|
+
)
|
119
|
+
).tap do |layer|
|
120
|
+
layer.selective = true
|
121
|
+
if schedule = notam.data[:five_day_schedules]
|
122
|
+
layer.timetable = timetable_from(schedule)
|
123
|
+
end
|
124
|
+
layer.remarks = notam.data[:translated_content]
|
125
|
+
end
|
126
|
+
)
|
127
|
+
airspace.comment = notam.text
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def geometry_from_content(notam)
|
132
|
+
if notam.data[:content].squish.match(/WI AREA(?<coordinates>(?: \d{6}N\d{7}E)+)/)
|
133
|
+
AIXM.geometry.tap do |geometry|
|
134
|
+
$~['coordinates'].split.each do |coordinate|
|
135
|
+
xy = AIXM.xy(lat: coordinate[0, 7], long: coordinate[7, 8])
|
136
|
+
geometry.add_segment(AIXM.point(xy: xy))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
else
|
140
|
+
warn "cannot parse WI AREA - fallback to point and radius"
|
141
|
+
geometry_from_q_item(notam)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def geometry_from_q_item(notam)
|
146
|
+
AIXM.geometry.tap do |geometry|
|
147
|
+
geometry.add_segment AIXM.circle(
|
148
|
+
center_xy: notam.data[:center_point],
|
149
|
+
radius: notam.data[:radius]
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def obstacle_from(notam)
|
155
|
+
# TODO: implement obstacle
|
156
|
+
end
|
157
|
+
|
158
|
+
def dabs_cross_check(added_notam_ids)
|
159
|
+
dabs_date = aixm.effective_at.to_date.strftime("DABS Date: %Y %^b %d")
|
160
|
+
case
|
161
|
+
when AIPP.cache.dabs.nil?
|
162
|
+
warn("DABS not available - skipping cross check")
|
163
|
+
when !AIPP.cache.dabs.text.include?(dabs_date)
|
164
|
+
warn("DABS date mismatch - skippping cross check")
|
165
|
+
else
|
166
|
+
dabs_notam_ids = AIPP.cache.dabs.text.scan(NOTAM::Item::ID_RE.decapture).uniq
|
167
|
+
missing_notam_ids = dabs_notam_ids - added_notam_ids
|
168
|
+
warn("DABS disagrees: #{missing_notam_ids.join(', ')} missing") if missing_notam_ids.any?
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
data/lib/aipp/runner.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
module AIPP
|
2
|
+
|
3
|
+
# @abstract
|
4
|
+
class Runner
|
5
|
+
include AIPP::Debugger
|
6
|
+
using AIXM::Refinements
|
7
|
+
|
8
|
+
# @return [AIXM::Document] target document
|
9
|
+
attr_reader :aixm
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
AIPP.options.storage = AIPP.options.storage.join(AIPP.options.region, AIPP.options.module.downcase)
|
13
|
+
AIPP.options.storage.mkpath
|
14
|
+
@dependencies = THash.new
|
15
|
+
@aixm = AIXM.document(effective_at: effective_at, expiration_at: expiration_at)
|
16
|
+
AIXM.send("#{AIPP.options.schema}!")
|
17
|
+
AIXM.config.region = AIPP.options.region
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def inspect
|
22
|
+
"#<#{self.class}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
# @abstract
|
26
|
+
def effective_at
|
27
|
+
fail "effective_at method must be implemented by module runner"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @abstract
|
31
|
+
def expiration_at
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# @abstract
|
36
|
+
def run
|
37
|
+
fail "run method must be implemented by module runner"
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Pathname] directory containing all files for the current region
|
41
|
+
def region_dir
|
42
|
+
Pathname(__FILE__).dirname.join('regions', AIPP.options.region)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String] sources file name (default: xmlschema representation
|
46
|
+
# of effective_at date/time)
|
47
|
+
def sources_file
|
48
|
+
effective_at.xmlschema
|
49
|
+
end
|
50
|
+
|
51
|
+
def output_file
|
52
|
+
"#{AIPP.options.region}_#{AIPP.options.module}_#{effective_at.strftime('%F_%HZ')}.#{AIPP.options.schema}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Pathname] directory containing the builds
|
56
|
+
def builds_dir
|
57
|
+
AIPP.options.storage.join('builds')
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Pathname] config file for the current region
|
61
|
+
def config_file
|
62
|
+
AIPP.options.storage.join('config.yml')
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Read the configuration from config.yml.
|
68
|
+
def read_config
|
69
|
+
info("reading config.yml")
|
70
|
+
AIPP.config.read! config_file
|
71
|
+
@aixm.namespace = AIPP.config.namespace
|
72
|
+
end
|
73
|
+
|
74
|
+
# Read the region directory.
|
75
|
+
def read_region
|
76
|
+
info("reading region #{AIPP.options.region}")
|
77
|
+
fail("unknown region `#{AIPP.options.region}'") unless region_dir.exist?
|
78
|
+
verbose_info "reading fixtures"
|
79
|
+
AIPP.fixtures.read! region_dir.join('fixtures')
|
80
|
+
verbose_info "reading borders"
|
81
|
+
AIPP.borders.read! region_dir.join('borders')
|
82
|
+
verbose_info "reading helpers"
|
83
|
+
region_dir.glob('helpers/*.rb').each { |f| require f }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Read parser files.
|
87
|
+
def read_parsers
|
88
|
+
verbose_info("reading parsers")
|
89
|
+
region_dir.join(AIPP.options.module.downcase).glob('*.rb').each do |file|
|
90
|
+
verbose_info "requiring #{file.basename}"
|
91
|
+
require file
|
92
|
+
section = file.basename('.*').to_s.classify
|
93
|
+
@dependencies[section] = class_for(section).dependencies
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Parse sections by invoking the parser classes.
|
98
|
+
def parse_sections
|
99
|
+
AIPP::Downloader.new(storage: AIPP.options.storage, source: sources_file) do |downloader|
|
100
|
+
@dependencies.tsort(AIPP.options.section).each do |section|
|
101
|
+
info("parsing #{section.sectionize}")
|
102
|
+
class_for(section).new(
|
103
|
+
downloader: downloader,
|
104
|
+
aixm: aixm
|
105
|
+
).attach_patches.tap(&:parse).detach_patches
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Validate the AIXM document.
|
111
|
+
#
|
112
|
+
# @raise [RuntimeError] if the document is not valid
|
113
|
+
def validate_aixm
|
114
|
+
info("detecting duplicates")
|
115
|
+
if (duplicates = aixm.features.duplicates).any?
|
116
|
+
message = "duplicates found"
|
117
|
+
details = duplicates.map { "#{_1.inspect} from #{_1.source}" }.join("\n")
|
118
|
+
AIPP.options.force ? warn(message) : fail([message, details].join(":\n"))
|
119
|
+
end
|
120
|
+
info("validating #{AIPP.options.schema.upcase}")
|
121
|
+
unless aixm.valid?
|
122
|
+
message = "invalid #{AIPP.options.schema.upcase} document"
|
123
|
+
details = aixm.errors.map(&:message).join("\n")
|
124
|
+
AIPP.options.force ? warn(message) : fail([message, details].join(":\n"))
|
125
|
+
end
|
126
|
+
info("counting #{aixm.features.count} features")
|
127
|
+
end
|
128
|
+
|
129
|
+
# Write the AIXM document.
|
130
|
+
def write_aixm(file)
|
131
|
+
info("writing #{file}")
|
132
|
+
AIXM.config.mid = AIPP.options.mid
|
133
|
+
File.write(file, aixm.to_xml)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Write build information.
|
137
|
+
def write_build
|
138
|
+
info ("skipping build")
|
139
|
+
end
|
140
|
+
|
141
|
+
# Write the configuration to config.yml.
|
142
|
+
def write_config
|
143
|
+
info("writing config.yml")
|
144
|
+
AIPP.config.write! config_file
|
145
|
+
end
|
146
|
+
|
147
|
+
def class_for(section)
|
148
|
+
[:AIPP, AIPP.options.region, AIPP.options.module.upcase, section.classify].constantize
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
data/lib/aipp/version.rb
CHANGED
data/lib/aipp.rb
CHANGED
@@ -1,46 +1,65 @@
|
|
1
1
|
require 'debug/session'
|
2
|
-
require '
|
2
|
+
require 'singleton'
|
3
3
|
require 'colorize'
|
4
4
|
require 'optparse'
|
5
5
|
require 'yaml'
|
6
|
-
require 'csv'
|
7
|
-
require 'roo'
|
8
6
|
require 'pathname'
|
7
|
+
require 'fileutils'
|
9
8
|
require 'tmpdir'
|
10
|
-
require 'net/http'
|
11
|
-
require 'open-uri'
|
12
9
|
require 'securerandom'
|
13
10
|
require 'tsort'
|
14
11
|
require 'ostruct'
|
15
12
|
require 'date'
|
13
|
+
require 'excon'
|
14
|
+
require 'graphql/client'
|
15
|
+
require 'graphql/client/http'
|
16
16
|
require 'nokogiri'
|
17
|
+
require 'csv'
|
18
|
+
require 'roo'
|
17
19
|
require 'pdf-reader'
|
18
20
|
require 'json'
|
19
21
|
require 'zip'
|
20
22
|
require 'airac'
|
21
23
|
require 'aixm'
|
24
|
+
require 'notam'
|
22
25
|
|
23
26
|
require 'active_support'
|
24
27
|
require 'active_support/core_ext/object/blank'
|
25
28
|
require 'active_support/core_ext/string'
|
29
|
+
require 'active_support/core_ext/date_time'
|
26
30
|
|
31
|
+
require_relative 'core_ext/nil_class'
|
27
32
|
require_relative 'core_ext/integer'
|
28
33
|
require_relative 'core_ext/string'
|
29
|
-
require_relative 'core_ext/
|
34
|
+
require_relative 'core_ext/array'
|
30
35
|
require_relative 'core_ext/enumerable'
|
31
36
|
require_relative 'core_ext/hash'
|
32
37
|
require_relative 'core_ext/nokogiri'
|
33
38
|
|
34
39
|
require_relative 'aipp/version'
|
35
40
|
require_relative 'aipp/debugger'
|
36
|
-
require_relative 'aipp/
|
41
|
+
require_relative 'aipp/downloader'
|
42
|
+
require_relative 'aipp/downloader/file'
|
43
|
+
require_relative 'aipp/downloader/http'
|
44
|
+
require_relative 'aipp/downloader/graphql'
|
45
|
+
require_relative 'aipp/patcher'
|
46
|
+
require_relative 'aipp/parser'
|
47
|
+
|
48
|
+
require_relative 'aipp/environment'
|
37
49
|
require_relative 'aipp/border'
|
50
|
+
require_relative 'aipp/pdf'
|
38
51
|
require_relative 'aipp/t_hash'
|
52
|
+
|
39
53
|
require_relative 'aipp/executable'
|
40
|
-
require_relative 'aipp/
|
41
|
-
|
42
|
-
require_relative 'aipp/
|
43
|
-
require_relative 'aipp/
|
54
|
+
require_relative 'aipp/runner'
|
55
|
+
|
56
|
+
require_relative 'aipp/aip/executable'
|
57
|
+
require_relative 'aipp/aip/runner'
|
58
|
+
require_relative 'aipp/aip/parser'
|
59
|
+
|
60
|
+
require_relative 'aipp/notam/executable'
|
61
|
+
require_relative 'aipp/notam/runner'
|
62
|
+
require_relative 'aipp/notam/parser'
|
44
63
|
|
45
64
|
# Disable "did you mean?" suggestions
|
46
65
|
#
|
data/lib/core_ext/nokogiri.rb
CHANGED
@@ -1,18 +1,26 @@
|
|
1
1
|
module Nokogiri
|
2
|
+
|
3
|
+
module PseudoClasses
|
4
|
+
class Matches
|
5
|
+
def matches(node_set, regexp)
|
6
|
+
node_set.find_all { _1.content.match?(/#{regexp}/) }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Pseudo class which matches the content of each node set member against the
|
12
|
+
# given regular expression.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# node.css('title:matches("\w+")', Nokogiri::MATCHES)
|
16
|
+
MATCHES = PseudoClasses::Matches.new
|
17
|
+
|
2
18
|
module XML
|
3
19
|
class Element
|
4
20
|
|
5
21
|
BOOLEANIZE_AS_TRUE_RE = /^(true|yes|oui|ja)$/i.freeze
|
6
22
|
BOOLEANIZE_AS_FALSE_RE = /^(false|no|non|nein)$/i.freeze
|
7
23
|
|
8
|
-
# Traverse all child elements and build a hash mapping the symbolized
|
9
|
-
# child node name to the child content.
|
10
|
-
#
|
11
|
-
# @return [Hash]
|
12
|
-
def contents
|
13
|
-
@contents ||= elements.to_h { [_1.name.to_sym, _1.content] }
|
14
|
-
end
|
15
|
-
|
16
24
|
# Shortcut to query +contents+ array which accepts both String or
|
17
25
|
# Symbol queries as well as query postfixes.
|
18
26
|
#
|
@@ -39,6 +47,46 @@ module Nokogiri
|
|
39
47
|
end
|
40
48
|
end
|
41
49
|
|
50
|
+
# Traverse all child elements and build a hash mapping the symbolized
|
51
|
+
# child node name to the child content.
|
52
|
+
#
|
53
|
+
# @return [Hash]
|
54
|
+
def contents
|
55
|
+
@contents ||= elements.to_h { [_1.name.to_sym, _1.content] }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Find this child element or add a new such element if none is found.
|
59
|
+
#
|
60
|
+
# The position to add is determined as follows:
|
61
|
+
#
|
62
|
+
# * If +after_css+ is given, its rules are applied in reverse order and
|
63
|
+
# the last matching rule defines the predecessor of the added child.
|
64
|
+
# * If only +before_css+ is given, its rules are applied in order and
|
65
|
+
# the first matching rule defines the successor of the added child.
|
66
|
+
# * If none of the above are given, the child is added at the end.
|
67
|
+
#
|
68
|
+
# @param name [Array<String>] name of the child element
|
69
|
+
# @param after_css [Array<String>] array of CSS rules
|
70
|
+
# @param before_css [Array<String>] array of CSS rules
|
71
|
+
# @return [Nokogiri::XML::Element, nil] element or +nil+ if none found
|
72
|
+
# and no position to add a new one could be determined
|
73
|
+
def find_or_add_child(name, after_css: nil, before_css: nil)
|
74
|
+
at_css(name) or begin
|
75
|
+
case
|
76
|
+
when after_css
|
77
|
+
at_css(*after_css.reverse).then do |predecessor|
|
78
|
+
predecessor&.add_next_sibling("<#{name}/>")
|
79
|
+
end&.first
|
80
|
+
when before_css
|
81
|
+
at_css(*before_css).then do |successor|
|
82
|
+
successor&.add_previous_sibling("<#{name}/>")
|
83
|
+
end&.first
|
84
|
+
else
|
85
|
+
add_child("<#{name}/>").first
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
42
90
|
private
|
43
91
|
|
44
92
|
def booleanize(content)
|
data/lib/core_ext/string.rb
CHANGED
@@ -1,6 +1,53 @@
|
|
1
1
|
class String
|
2
|
+
remove_method :classify
|
2
3
|
|
3
|
-
# Convert
|
4
|
+
# Convert (underscored) file name to (camelcased) class name
|
5
|
+
#
|
6
|
+
# Similar to +classify+ from ActiveSupport, however, with a few differences:
|
7
|
+
#
|
8
|
+
# * Namespaces are ignored.
|
9
|
+
# * Plural strings are not singularized.
|
10
|
+
# * Characters other than A-Z, a-z, 0-9 and _ are removed.
|
11
|
+
#
|
12
|
+
# Use +sectionize+ to reverse this method.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# "navigational_aids".classify # => "NavigationalAids"
|
16
|
+
# "ENR".classify # => "ENR"
|
17
|
+
# "ENR-4.1".classify # => "ENR41"
|
18
|
+
# "AIPP/LF/AIP/ENR-4.1".classify # => "ENR41"
|
19
|
+
#
|
20
|
+
# @return [String] converted string
|
21
|
+
def classify
|
22
|
+
split('/').last.remove(/\W/).camelcase
|
23
|
+
end
|
24
|
+
|
25
|
+
# Convert (camelcased) class name to (underscored) file name
|
26
|
+
#
|
27
|
+
# Similar to +underscore+ from ActiveSupport, however, with a few differences:
|
28
|
+
#
|
29
|
+
# * Namespaces are ignored.
|
30
|
+
# * AIP naming conventions are honored.
|
31
|
+
#
|
32
|
+
# Use +classify+ to reverse this method.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# "NavigationalAids".sectionize # => "navigational_aids"
|
36
|
+
# "ENR".sectionize # => "ENR"
|
37
|
+
# "ENR41".sectionize # => "ENR-4.1"
|
38
|
+
# "AIPP::LF::AIP::ENR41".sectionize # => "ENR-4.1"
|
39
|
+
#
|
40
|
+
# @return [String] converted string
|
41
|
+
def sectionize
|
42
|
+
case klass = self.split('::').last
|
43
|
+
when /\A([A-Z]{2,3})\z/ then $1
|
44
|
+
when /\A([A-Z]{2,3})(\d)\z/ then "#{$1}-#{$2}"
|
45
|
+
when /\A([A-Z]{2,3})(\d)(\d+)\z/ then "#{$1}-#{$2}.#{$3}"
|
46
|
+
else klass.underscore
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Convert blank strings to +nil+
|
4
51
|
#
|
5
52
|
# @example
|
6
53
|
# "foobar".blank_to_nil # => "foobar"
|
@@ -77,10 +124,25 @@ class String
|
|
77
124
|
end
|
78
125
|
|
79
126
|
# Remove all XML/HTML tags and entities from the string
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# "this <em>is</em> a <br> test".strip_markup # => "this is a test"
|
130
|
+
#
|
131
|
+
# @return [String]
|
80
132
|
def strip_markup
|
81
133
|
self.gsub(/<.*?>|&[#\da-z]+;/i, '')
|
82
134
|
end
|
83
135
|
|
136
|
+
# Builds the MD5 hash as hex and returns the first eight characters.
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# "this is a test".to_digest # => "54b0c58c"
|
140
|
+
#
|
141
|
+
# @return [String]
|
142
|
+
def to_digest
|
143
|
+
Digest::MD5.hexdigest(self)[0,8]
|
144
|
+
end
|
145
|
+
|
84
146
|
# Same as +to_f+ but accept both dot and comma as decimal separator
|
85
147
|
#
|
86
148
|
# @example
|
data.tar.gz.sig
CHANGED
Binary file
|