aipp 0.2.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +21 -0
- data/README.md +147 -91
- data/exe/aip2aixm +2 -2
- data/exe/aip2ofmx +2 -2
- data/lib/aipp/aip.rb +96 -11
- data/lib/aipp/border.rb +77 -46
- data/lib/aipp/debugger.rb +101 -0
- data/lib/aipp/downloader.rb +18 -5
- data/lib/aipp/executable.rb +33 -20
- data/lib/aipp/parser.rb +42 -37
- data/lib/aipp/patcher.rb +5 -2
- 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 +167 -174
- 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 +3 -4
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +7 -5
- data/lib/core_ext/enumerable.rb +2 -2
- data/lib/core_ext/hash.rb +21 -5
- data/lib/core_ext/nokogiri.rb +54 -0
- data/lib/core_ext/string.rb +32 -65
- data.tar.gz.sig +0 -0
- metadata +70 -81
- metadata.gz.sig +0 -0
- data/lib/aipp/airac.rb +0 -55
- data/lib/aipp/regions/LF/AD-1.3.rb +0 -177
- data/lib/aipp/regions/LF/AD-1.6.rb +0 -33
- data/lib/aipp/regions/LF/AD-2.rb +0 -344
- data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
- data/lib/aipp/regions/LF/ENR-2.1.rb +0 -167
- data/lib/aipp/regions/LF/ENR-4.1.rb +0 -41
- data/lib/aipp/regions/LF/ENR-4.3.rb +0 -27
- data/lib/aipp/regions/LF/ENR-5.1.rb +0 -106
- data/lib/aipp/regions/LF/ENR-5.4.rb +0 -90
- data/lib/aipp/regions/LF/ENR-5.5.rb +0 -55
- 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/URL.rb +0 -26
- data/lib/aipp/regions/LF/helpers/navigational_aid.rb +0 -104
- data/lib/aipp/regions/LF/helpers/radio_AD.rb +0 -110
- data/lib/core_ext/object.rb +0 -43
@@ -0,0 +1,153 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
|
4
|
+
class Obstacles < AIP
|
5
|
+
|
6
|
+
include AIPP::LF::Helpers::Base
|
7
|
+
|
8
|
+
# Map type descriptions to AIXM types and remarks
|
9
|
+
TYPES = {
|
10
|
+
'Antenne' => [:antenna],
|
11
|
+
'Autre' => [:other],
|
12
|
+
'Bâtiment' => [:building],
|
13
|
+
'Câble' => [:other, 'Cable / Câble'],
|
14
|
+
'Centrale thermique' => [:building, 'Thermal power plant / Centrale thermique'],
|
15
|
+
"Château d'eau" => [:tower, "Water tower / Château d'eau"],
|
16
|
+
'Cheminée' => [:chimney],
|
17
|
+
'Derrick' => [:tower, 'Derrick'],
|
18
|
+
'Eglise' => [:tower, 'Church / Eglise'],
|
19
|
+
'Eolienne' => [:wind_turbine],
|
20
|
+
'Eolienne(s)' => [:wind_turbine],
|
21
|
+
'Grue' => [:tower, 'Crane / Grue'],
|
22
|
+
'Mât' => [:mast],
|
23
|
+
'Phare marin' => [:tower, 'Lighthouse / Phare marin'],
|
24
|
+
'Pile de pont' => [:other, 'Bridge piers / Pile de pont'],
|
25
|
+
'Portique' => [:building, 'Arch / Portique'],
|
26
|
+
'Pylône' => [:mast, 'Pylon / Pylône'],
|
27
|
+
'Silo' => [:tower, 'Silo'],
|
28
|
+
'Terril' => [:other, 'Spoil heap / Teril'],
|
29
|
+
'Torchère' => [:chimney, 'Flare / Torchère'],
|
30
|
+
'Tour' => [:tower],
|
31
|
+
'Treillis métallique' => [:other, 'Metallic grid / Treillis métallique']
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
def parse
|
35
|
+
if options[:region_options].include? 'lf_obstacles_xlsx'
|
36
|
+
info("reading obstacles from XLSX")
|
37
|
+
@xlsx = read('Obstacles')
|
38
|
+
parse_from_xlsx
|
39
|
+
else
|
40
|
+
parse_from_xml
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse_from_xlsx
|
47
|
+
# Build obstacles
|
48
|
+
@xlsx.sheet(@xlsx.sheets.find(/^data/i).first).each(
|
49
|
+
name: 'IDENTIFICATEUR',
|
50
|
+
type: 'TYPE',
|
51
|
+
count: 'NOMBRE',
|
52
|
+
longitude: 'LONGITUDE DECIMALE',
|
53
|
+
latitude: 'LATITUDE DECIMALE',
|
54
|
+
elevation: 'ALTITUDE AU SOMMET',
|
55
|
+
height: 'HAUTEUR HORS SOL',
|
56
|
+
height_unit: 'UNITE',
|
57
|
+
horizontal_accuracy: 'PRECISION HORIZONTALE',
|
58
|
+
vertical_accuracy: 'PRECISION VERTICALE',
|
59
|
+
visibility: 'BALISAGE',
|
60
|
+
remarks: 'REMARK',
|
61
|
+
effective_on: 'DATE DE MISE EN VIGUEUR'
|
62
|
+
).with_index(0) do |row, index|
|
63
|
+
next unless row[:effective_on].to_s.match? /\d{8}/
|
64
|
+
type, type_remarks = TYPES.fetch(row[:type])
|
65
|
+
count = row[:count].to_i
|
66
|
+
obstacle = AIXM.obstacle(
|
67
|
+
source: source(section: 'ENR', position: index),
|
68
|
+
name: row[:name],
|
69
|
+
type: type,
|
70
|
+
xy: AIXM.xy(lat: row[:latitude].to_f, long: row[:longitude].to_f),
|
71
|
+
z: AIXM.z(row[:elevation].to_i, :qnh)
|
72
|
+
).tap do |obstacle|
|
73
|
+
obstacle.height = AIXM.d(row[:height].to_i, row[:height_unit])
|
74
|
+
if row[:horizontal_accuracy]
|
75
|
+
accuracy = row[:horizontal_accuracy].split
|
76
|
+
obstacle.xy_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
|
77
|
+
end
|
78
|
+
if row[:vertical_accuracy]
|
79
|
+
accuracy = row[:horizontal_accuracy].split
|
80
|
+
obstacle.z_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
|
81
|
+
end
|
82
|
+
obstacle.marking = row[:visibility].match?(/jour/i)
|
83
|
+
obstacle.lighting = row[:visibility].match?(/nuit/i)
|
84
|
+
obstacle.remarks = {
|
85
|
+
'type' => type_remarks,
|
86
|
+
'number/nombre' => (count if count > 1),
|
87
|
+
'details' => row[:remarks],
|
88
|
+
'effective/mise en vigueur' => (row[:effective_on].to_s.unpack("a4a2a2").join("-") if row[:updated_on])
|
89
|
+
}.to_remarks
|
90
|
+
# Group obstacles
|
91
|
+
if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
|
92
|
+
warn("duplicate obstacle #{obstacle.name}", severe: false)
|
93
|
+
else
|
94
|
+
if count > 1
|
95
|
+
obstacle_group = AIXM.obstacle_group(
|
96
|
+
source: obstacle.source,
|
97
|
+
name: obstacle.name
|
98
|
+
).tap do |obstacle_group|
|
99
|
+
obstacle_group.remarks = "#{count} obstacles"
|
100
|
+
end
|
101
|
+
obstacle_group.add_obstacle obstacle
|
102
|
+
add obstacle_group
|
103
|
+
else
|
104
|
+
add obstacle
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_from_xml
|
112
|
+
cache.obstacle.css(%Q(Obstacle[lk^="[LF]"])).each do |node|
|
113
|
+
# Build obstacles
|
114
|
+
type, type_remarks = TYPES.fetch(node.(:TypeObst))
|
115
|
+
count = node.(:Combien).to_i
|
116
|
+
obstacle = AIXM.obstacle(
|
117
|
+
source: source(section: 'ENR', position: node.line),
|
118
|
+
name: node.(:NumeroNom),
|
119
|
+
type: type,
|
120
|
+
xy: xy_from(node.(:Geometrie)),
|
121
|
+
z: AIXM.z(node.(:AmslFt).to_i, :qnh)
|
122
|
+
).tap do |obstacle|
|
123
|
+
obstacle.height = AIXM.d(node.(:AglFt).to_i, :ft)
|
124
|
+
obstacle.marking = node.(:Balisage).match?(/jour/i)
|
125
|
+
obstacle.lighting = node.(:Balisage).match?(/nuit/i)
|
126
|
+
obstacle.remarks = {
|
127
|
+
'type' => type_remarks,
|
128
|
+
'number/nombre' => (count if count > 1)
|
129
|
+
}.to_remarks
|
130
|
+
end
|
131
|
+
# Group obstacles
|
132
|
+
if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
|
133
|
+
warn("duplicate obstacle #{obstacle.name}", severe: false)
|
134
|
+
else
|
135
|
+
if count > 1
|
136
|
+
obstacle_group = AIXM.obstacle_group(
|
137
|
+
source: obstacle.source,
|
138
|
+
name: obstacle.name
|
139
|
+
).tap do |obstacle_group|
|
140
|
+
obstacle_group.remarks = "#{count} obstacles"
|
141
|
+
end
|
142
|
+
obstacle_group.add_obstacle obstacle
|
143
|
+
add obstacle_group
|
144
|
+
else
|
145
|
+
add obstacle
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
|
4
|
+
class ServicedAirspaces < AIP
|
5
|
+
|
6
|
+
include AIPP::LF::Helpers::Base
|
7
|
+
|
8
|
+
# Map source types to type and optional local type and skip regexp
|
9
|
+
SOURCE_TYPES = {
|
10
|
+
'FIR' => { type: 'FIR' },
|
11
|
+
'UIR' => { type: 'UIR' },
|
12
|
+
'UTA' => { type: 'UTA' },
|
13
|
+
'CTA' => { type: 'CTA' },
|
14
|
+
'LTA' => { type: 'CTA', local_type: 'LTA' },
|
15
|
+
'TMA' => { type: 'TMA', skip: /geneve/i }, # Geneva listed FYI only
|
16
|
+
'SIV' => { type: 'SECTOR', local_type: 'FIZ/SIV' }, # providing FIS
|
17
|
+
'CTR' => { type: 'CTR' },
|
18
|
+
'RMZ' => { type: 'RAS', local_type: 'RMZ' },
|
19
|
+
'TMZ' => { type: 'RAS', local_type: 'TMZ' },
|
20
|
+
'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
# Map airspace "<type> <name>" to location indicator
|
24
|
+
FIR_LOCATION_INDICATORS = {
|
25
|
+
'BORDEAUX' => 'LFBB',
|
26
|
+
'BREST' => 'LFRR',
|
27
|
+
'MARSEILLE' => 'LFMM',
|
28
|
+
'PARIS' => 'LFFF',
|
29
|
+
'REIMS' => 'LFRR'
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
def parse
|
33
|
+
SOURCE_TYPES.each do |source_type, target|
|
34
|
+
verbose_info("processing #{source_type}")
|
35
|
+
cache.espace.css(%Q(Espace[lk^="[LF][#{source_type} "])).each do |espace_node|
|
36
|
+
# Skip all delegated airspaces
|
37
|
+
next if espace_node.(:Nom).match? /deleg/i
|
38
|
+
next if (re = target[:skip]) && espace_node.(:Nom).match?(re)
|
39
|
+
# Build airspaces and layers
|
40
|
+
cache.partie.css(%Q(Partie:has(Espace[pk="#{espace_node['pk']}"]))).each do |partie_node|
|
41
|
+
add(
|
42
|
+
AIXM.airspace(
|
43
|
+
source: source(section: 'ENR', position: espace_node.line),
|
44
|
+
name: [
|
45
|
+
espace_node.(:Nom),
|
46
|
+
partie_node.(:NomPartie).remove(/^\.$/).blank_to_nil
|
47
|
+
].compact.join(' '),
|
48
|
+
type: target[:type],
|
49
|
+
local_type: target[:local_type]
|
50
|
+
).tap do |airspace|
|
51
|
+
airspace.meta = espace_node.attr('pk')
|
52
|
+
airspace.geometry = geometry_from(partie_node.(:Contour))
|
53
|
+
fail("geometry is not closed") unless airspace.geometry.closed?
|
54
|
+
cache.volume.css(%Q(Volume:has(Partie[pk="#{partie_node['pk']}"]))).each do |volume_node|
|
55
|
+
airspace.add_layer(
|
56
|
+
layer_from(volume_node).tap do |layer|
|
57
|
+
layer.location_indicator = FIR_LOCATION_INDICATORS.fetch(airspace.name) if airspace.type == :flight_information_region
|
58
|
+
end
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module AIPP
|
2
|
+
module LF
|
3
|
+
|
4
|
+
class Services < AIP
|
5
|
+
|
6
|
+
include AIPP::LF::Helpers::Base
|
7
|
+
|
8
|
+
DEPENDS = %w(aerodromes serviced_airspaces)
|
9
|
+
|
10
|
+
# Service types and how to treat them
|
11
|
+
SOURCE_TYPES = {
|
12
|
+
'A/A' => :address,
|
13
|
+
'AFIS' => :service,
|
14
|
+
'APP' => :service,
|
15
|
+
'ATIS' => :service,
|
16
|
+
'CCM' => :ignore, # centre de contrôle militaire
|
17
|
+
'CEV' => :ignore, # centre d’essai en vol
|
18
|
+
'D-ATIS' => :ignore, # data link ATIS
|
19
|
+
'FIS' => :service,
|
20
|
+
'PAR' => :service,
|
21
|
+
'SRE' => :ignore, # surveillance radar element of PAR
|
22
|
+
'TWR' => :service,
|
23
|
+
'UAC' => :ignore, # upper area control
|
24
|
+
'VDF' => :service,
|
25
|
+
'OTHER' => :service # no <Service> specified at source
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
# Map French callsigns to English and service type
|
29
|
+
CALLSIGNS = {
|
30
|
+
'Approche' => { en: 'Approach', service_type: 'APP' },
|
31
|
+
'Contrôle' => { en: 'Control', service_type: 'ACS' },
|
32
|
+
'Information' => { en: 'Information', service_type: 'FIS' },
|
33
|
+
'GCA' => { en: 'GCA', service_type: 'GCA' },
|
34
|
+
'Gonio' => { en: 'Gonio', service_type: 'VDF' },
|
35
|
+
'Prévol' => { en: 'Delivery', service_type: 'SMC' },
|
36
|
+
'Sol' => { en: 'Ground', service_type: 'SMC' },
|
37
|
+
'Tour' => { en: 'Tower', service_type: 'TWR' },
|
38
|
+
'Trafic' => { en: 'Apron', service_type: 'SMC' }
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
def parse
|
42
|
+
cache.service.css(%Q(Service[lk^="[LF]"][pk])).each do |service_node|
|
43
|
+
# Ignore private services/frequencies
|
44
|
+
next if service_node.(:IndicLieu) == 'XX'
|
45
|
+
# Load referenced airport
|
46
|
+
airport = given(service_node.at_css('Ad')&.attr('pk')) do
|
47
|
+
find_by(:airport, meta: _1).first
|
48
|
+
end
|
49
|
+
# Build addresses and services
|
50
|
+
case SOURCE_TYPES.fetch(type_for(service_node))
|
51
|
+
when :address
|
52
|
+
fail "dangling address without airport" unless airport
|
53
|
+
addresses_from(service_node).each { airport.add_address(_1) }
|
54
|
+
when :service
|
55
|
+
given service_from(service_node) do |service|
|
56
|
+
cache.frequence.css(%Q(Frequence:has(Service[pk="#{service_node['pk']}"]))).each do |frequence_node|
|
57
|
+
if frequency = frequency_from(frequence_node, service_node)
|
58
|
+
service.add_frequency frequency
|
59
|
+
end
|
60
|
+
end
|
61
|
+
if airport
|
62
|
+
airport.add_unit(service.unit) if airport.units.find(service.unit).none?
|
63
|
+
airport.add_service(service) if airport.services.find(service).none?
|
64
|
+
end
|
65
|
+
given service_node.at_css('Espace')&.attr('pk') do |espace_pk|
|
66
|
+
find_by(:airspace, meta: espace_pk).each do |airspace|
|
67
|
+
airspace.layers.each { _1.add_service(service) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# Assign fallback address (default A/A frequency) to all yet radioless airports
|
74
|
+
find_by(:airport).each do |airport|
|
75
|
+
unless airport.services.find_by(:service, type: :aerodrome_control_tower_service).any? || airport.addresses.any?
|
76
|
+
airport.add_address(fallback_address_for(airport.name))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def type_for(service_node)
|
84
|
+
SOURCE_TYPES.include?(type = service_node.(:Service)) ? type : 'OTHER'
|
85
|
+
end
|
86
|
+
|
87
|
+
def addresses_from(service_node)
|
88
|
+
cache.frequence.css(%Q(Frequence:has(Service[pk="#{service_node['pk']}"]))).map do |frequence_node|
|
89
|
+
if frequency = frequency_from(frequence_node, service_node)
|
90
|
+
AIXM.address(
|
91
|
+
type: :radio_frequency,
|
92
|
+
address: frequency.transmission_f
|
93
|
+
).tap do |address|
|
94
|
+
address.remarks = {
|
95
|
+
'type' => service_node.(:Service),
|
96
|
+
'indicatif/callsign' => frequency.callsigns.map { "#{_2} (#{_1})" }.join("/n")
|
97
|
+
}.to_remarks
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end.compact
|
101
|
+
end
|
102
|
+
|
103
|
+
def fallback_address_for(callsign)
|
104
|
+
AIXM.address(
|
105
|
+
type: :radio_frequency,
|
106
|
+
address: AIXM.f(123.5, :mhz)
|
107
|
+
).tap do |address|
|
108
|
+
address.remarks = {
|
109
|
+
'type' => 'A/A',
|
110
|
+
'indicatif/callsign' => callsign
|
111
|
+
}.to_remarks
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def service_from(service_node)
|
116
|
+
service_type = CALLSIGNS.dig(service_node.(:IndicService), :service_type) || type_for(service_node)
|
117
|
+
service = find_by(:service, type: service_type).first
|
118
|
+
unit = service&.unit
|
119
|
+
unless service
|
120
|
+
service = AIXM.service(type: service_type)
|
121
|
+
unit = find_by(:unit, name: service_node.(:IndicLieu), type: service.guessed_unit_type).first
|
122
|
+
unless unit
|
123
|
+
unit = AIXM.unit(
|
124
|
+
source: source(section: 'GEN', position: service_node.line),
|
125
|
+
organisation: organisation_lf,
|
126
|
+
name: service_node.(:IndicLieu),
|
127
|
+
type: service.guessed_unit_type,
|
128
|
+
class: :icao
|
129
|
+
)
|
130
|
+
add unit
|
131
|
+
end
|
132
|
+
unit.add_service service
|
133
|
+
service
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def frequency_from(frequence_node, service_node)
|
138
|
+
frequency = frequence_node.(:Frequence).to_f
|
139
|
+
case
|
140
|
+
when frequency >= 137
|
141
|
+
nil
|
142
|
+
when frequency < 108
|
143
|
+
warn("ignoring too low frequency `#{frequency}'", severe: false)
|
144
|
+
nil
|
145
|
+
else
|
146
|
+
AIXM.frequency(
|
147
|
+
transmission_f: AIXM.f(frequency, :mhz),
|
148
|
+
callsigns: callsigns_from(service_node)
|
149
|
+
).tap do |frequency|
|
150
|
+
frequency.timetable = timetable_from(frequence_node.(:HorCode))
|
151
|
+
frequency.remarks = frequence_node.(:Remarque)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def callsigns_from(service_node)
|
157
|
+
if service_node.(:IndicService) == '.' # auto-information
|
158
|
+
%i(fr en).to_h { [_1, '(auto)'] }
|
159
|
+
else
|
160
|
+
warn("language other than french") unless service_node.(:Langue) == 'fr'
|
161
|
+
english = CALLSIGNS.fetch(service_node.(:IndicService)).fetch(:en)
|
162
|
+
warn("no english translation for callsign `#{service_node.(:IndicService)}'") unless english
|
163
|
+
{
|
164
|
+
fr: "#{service_node.(:IndicLieu)} #{service_node.(:IndicService)}",
|
165
|
+
en: ("#{service_node.(:IndicLieu)} #{english}" if english)
|
166
|
+
}.compact
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/lib/aipp/t_hash.rb
CHANGED
@@ -18,8 +18,8 @@ module AIPP
|
|
18
18
|
|
19
19
|
alias_method :tsort_each_node, :each_key
|
20
20
|
|
21
|
-
def tsort_each_child(node, &
|
22
|
-
fetch(node).each(&
|
21
|
+
def tsort_each_child(node, &)
|
22
|
+
fetch(node).each(&)
|
23
23
|
end
|
24
24
|
|
25
25
|
def tsort(node=nil)
|
@@ -35,9 +35,8 @@ module AIPP
|
|
35
35
|
|
36
36
|
def subhash_for(node, memo=[])
|
37
37
|
memo.tap do |m|
|
38
|
-
fail TSort::Cyclic if m.include? node
|
39
38
|
m << node
|
40
|
-
fetch(node).each { subhash_for(_1, m) }
|
39
|
+
(fetch(node) - m).each { subhash_for(_1, m) }
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
data/lib/aipp/version.rb
CHANGED
data/lib/aipp.rb
CHANGED
@@ -1,40 +1,42 @@
|
|
1
|
+
require 'debug/session'
|
1
2
|
require 'forwardable'
|
2
3
|
require 'colorize'
|
3
|
-
require 'pry'
|
4
|
-
require 'pry-rescue'
|
5
4
|
require 'optparse'
|
6
5
|
require 'yaml'
|
7
6
|
require 'csv'
|
7
|
+
require 'roo'
|
8
8
|
require 'pathname'
|
9
9
|
require 'tmpdir'
|
10
|
+
require 'net/http'
|
10
11
|
require 'open-uri'
|
11
12
|
require 'securerandom'
|
12
13
|
require 'tsort'
|
13
14
|
require 'ostruct'
|
14
15
|
require 'date'
|
15
16
|
require 'nokogiri'
|
16
|
-
require 'nokogumbo'
|
17
17
|
require 'pdf-reader'
|
18
18
|
require 'json'
|
19
19
|
require 'zip'
|
20
|
+
require 'airac'
|
20
21
|
require 'aixm'
|
21
22
|
|
22
23
|
require 'active_support'
|
23
24
|
require 'active_support/core_ext/object/blank'
|
24
25
|
require 'active_support/core_ext/string'
|
25
|
-
|
26
|
+
|
26
27
|
require_relative 'core_ext/integer'
|
27
28
|
require_relative 'core_ext/string'
|
28
29
|
require_relative 'core_ext/nil_class'
|
29
30
|
require_relative 'core_ext/enumerable'
|
30
31
|
require_relative 'core_ext/hash'
|
32
|
+
require_relative 'core_ext/nokogiri'
|
31
33
|
|
32
34
|
require_relative 'aipp/version'
|
35
|
+
require_relative 'aipp/debugger'
|
33
36
|
require_relative 'aipp/pdf'
|
34
37
|
require_relative 'aipp/border'
|
35
38
|
require_relative 'aipp/t_hash'
|
36
39
|
require_relative 'aipp/executable'
|
37
|
-
require_relative 'aipp/airac'
|
38
40
|
require_relative 'aipp/patcher'
|
39
41
|
require_relative 'aipp/aip'
|
40
42
|
require_relative 'aipp/parser'
|
data/lib/core_ext/enumerable.rb
CHANGED
@@ -20,9 +20,9 @@ module Enumerable
|
|
20
20
|
# @yield [Object] element to analyze
|
21
21
|
# @yieldreturn [Boolean] whether to split at this element or not
|
22
22
|
# @return [Array]
|
23
|
-
def split(*args, &
|
23
|
+
def split(*args, &)
|
24
24
|
[].tap do |array|
|
25
|
-
while index = slice((start ||= 0)...length).find_index(*args, &
|
25
|
+
while index = slice((start ||= 0)...length).find_index(*args, &)
|
26
26
|
array << slice(start...start+index)
|
27
27
|
start += index + 1
|
28
28
|
end
|
data/lib/core_ext/hash.rb
CHANGED
@@ -4,8 +4,8 @@ class Hash
|
|
4
4
|
#
|
5
5
|
# Similar to +fetch+, search the hash keys for the search string and return
|
6
6
|
# the corresponding value. Unlike +fetch+, however, if a hash key is a Regexp,
|
7
|
-
# the search
|
8
|
-
# in
|
7
|
+
# the search argument is matched against this Regexp. The hash is searched
|
8
|
+
# in its natural order.
|
9
9
|
#
|
10
10
|
# @example
|
11
11
|
# h = { /aa/ => :aa, /a/ => :a, 'b' => :b }
|
@@ -18,14 +18,30 @@ class Hash
|
|
18
18
|
# @param default [Object] fallback value if no key matched
|
19
19
|
# @return [Object] hash value
|
20
20
|
# @raise [KeyError] no key matched and no default given
|
21
|
-
def metch(search, default
|
21
|
+
def metch(search, default=:__n_o_n_e__)
|
22
22
|
fetch search
|
23
23
|
rescue KeyError
|
24
24
|
each do |key, value|
|
25
25
|
next unless key.is_a? Regexp
|
26
|
-
return value if
|
26
|
+
return value if key.match? search
|
27
27
|
end
|
28
|
-
|
28
|
+
raise(KeyError, "no match found: #{search.inspect}") if default == :__n_o_n_e__
|
29
|
+
default
|
29
30
|
end
|
30
31
|
|
32
|
+
# Compile a titles/texts hash to remarks Markdown string
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# { name: 'foobar', ignore: => nil, 'count/quantité' => 3 }.to_remarks
|
36
|
+
# # => "NAME\nfoobar\n\nCOUNT/QUANTITÉ\n3"
|
37
|
+
# { ignore: nil, ignore_as_well: "" }.to_remarks
|
38
|
+
# # => nil
|
39
|
+
#
|
40
|
+
# @return [String, nil] compiled remarks
|
41
|
+
def to_remarks
|
42
|
+
map { |k, v| "**#{k.to_s.upcase}**\n#{v}" unless v.blank? }.
|
43
|
+
compact.
|
44
|
+
join("\n\n").
|
45
|
+
blank_to_nil
|
46
|
+
end
|
31
47
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Nokogiri
|
2
|
+
module XML
|
3
|
+
class Element
|
4
|
+
|
5
|
+
BOOLEANIZE_AS_TRUE_RE = /^(true|yes|oui|ja)$/i.freeze
|
6
|
+
BOOLEANIZE_AS_FALSE_RE = /^(false|no|non|nein)$/i.freeze
|
7
|
+
|
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
|
+
# Shortcut to query +contents+ array which accepts both String or
|
17
|
+
# Symbol queries as well as query postfixes.
|
18
|
+
#
|
19
|
+
# @example query optional content for :key
|
20
|
+
# element.(:key) # same as element.contents[:key]
|
21
|
+
#
|
22
|
+
# @example query mandatory content for :key
|
23
|
+
# element.(:key!) # fails if the key does not exist
|
24
|
+
#
|
25
|
+
# @example query boolean content for :key
|
26
|
+
# element.(:key?) # returns true or false
|
27
|
+
#
|
28
|
+
# @see +BOOLEANIZE_AS_TRUE_RE+ and +BOOLEANIZE_AS_FALSE_RE+ define the
|
29
|
+
# regular expressions which convert the content to boolean. Furthermore,
|
30
|
+
# nil is interpreted as false as well.
|
31
|
+
#
|
32
|
+
# @raise KeyError mandatory or boolean content not found
|
33
|
+
# @return [String, Boolean]
|
34
|
+
def call(query)
|
35
|
+
case query
|
36
|
+
when /\?$/ then booleanize(contents.fetch(query[...-1].to_sym))
|
37
|
+
when /\!$/ then contents.fetch(query[...-1].to_sym)
|
38
|
+
else contents[query.to_sym]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def booleanize(content)
|
45
|
+
case content
|
46
|
+
when nil then false
|
47
|
+
when BOOLEANIZE_AS_TRUE_RE then true
|
48
|
+
when BOOLEANIZE_AS_FALSE_RE then false
|
49
|
+
else fail(KeyError, "`#{content}' not recognized as boolean")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|