aipp 1.0.0 → 2.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 -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
|