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,150 @@
|
|
1
|
+
module AIPP::LF::AIP
|
2
|
+
class Obstacles < AIPP::AIP::Parser
|
3
|
+
|
4
|
+
include AIPP::LF::Helpers::Base
|
5
|
+
|
6
|
+
# Map type descriptions to AIXM types and remarks
|
7
|
+
TYPES = {
|
8
|
+
'Antenne' => [:antenna],
|
9
|
+
'Autre' => [:other],
|
10
|
+
'Bâtiment' => [:building],
|
11
|
+
'Câble' => [:other, 'Cable / Câble'],
|
12
|
+
'Centrale thermique' => [:building, 'Thermal power plant / Centrale thermique'],
|
13
|
+
"Château d'eau" => [:tower, "Water tower / Château d'eau"],
|
14
|
+
'Cheminée' => [:chimney],
|
15
|
+
'Derrick' => [:tower, 'Derrick'],
|
16
|
+
'Eglise' => [:tower, 'Church / Eglise'],
|
17
|
+
'Eolienne' => [:wind_turbine],
|
18
|
+
'Eolienne(s)' => [:wind_turbine],
|
19
|
+
'Grue' => [:tower, 'Crane / Grue'],
|
20
|
+
'Mât' => [:mast],
|
21
|
+
'Phare marin' => [:tower, 'Lighthouse / Phare marin'],
|
22
|
+
'Pile de pont' => [:other, 'Bridge piers / Pile de pont'],
|
23
|
+
'Portique' => [:building, 'Arch / Portique'],
|
24
|
+
'Pylône' => [:mast, 'Pylon / Pylône'],
|
25
|
+
'Silo' => [:tower, 'Silo'],
|
26
|
+
'Terril' => [:other, 'Spoil heap / Teril'],
|
27
|
+
'Torchère' => [:chimney, 'Flare / Torchère'],
|
28
|
+
'Tour' => [:tower],
|
29
|
+
'Treillis métallique' => [:other, 'Metallic grid / Treillis métallique']
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
def parse
|
33
|
+
if AIPP.options.region_options.include? 'lf_obstacles_xlsx'
|
34
|
+
info("reading obstacles from XLSX")
|
35
|
+
@xlsx = read('Obstacles')
|
36
|
+
parse_from_xlsx
|
37
|
+
else
|
38
|
+
parse_from_xml
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def parse_from_xlsx
|
45
|
+
# Build obstacles
|
46
|
+
@xlsx.sheet(@xlsx.sheets.find(/^data/i).first).each(
|
47
|
+
name: 'IDENTIFICATEUR',
|
48
|
+
type: 'TYPE',
|
49
|
+
count: 'NOMBRE',
|
50
|
+
longitude: 'LONGITUDE DECIMALE',
|
51
|
+
latitude: 'LATITUDE DECIMALE',
|
52
|
+
elevation: 'ALTITUDE AU SOMMET',
|
53
|
+
height: 'HAUTEUR HORS SOL',
|
54
|
+
height_unit: 'UNITE',
|
55
|
+
horizontal_accuracy: 'PRECISION HORIZONTALE',
|
56
|
+
vertical_accuracy: 'PRECISION VERTICALE',
|
57
|
+
visibility: 'BALISAGE',
|
58
|
+
remarks: 'REMARK',
|
59
|
+
effective_on: 'DATE DE MISE EN VIGUEUR'
|
60
|
+
).with_index(0) do |row, index|
|
61
|
+
next unless row[:effective_on].to_s.match? /\d{8}/
|
62
|
+
type, type_remarks = TYPES.fetch(row[:type])
|
63
|
+
count = row[:count].to_i
|
64
|
+
obstacle = AIXM.obstacle(
|
65
|
+
source: source(part: 'ENR', position: index),
|
66
|
+
name: row[:name],
|
67
|
+
type: type,
|
68
|
+
xy: AIXM.xy(lat: row[:latitude].to_f, long: row[:longitude].to_f),
|
69
|
+
z: AIXM.z(row[:elevation].to_i, :qnh)
|
70
|
+
).tap do |obstacle|
|
71
|
+
obstacle.height = AIXM.d(row[:height].to_i, row[:height_unit])
|
72
|
+
if row[:horizontal_accuracy]
|
73
|
+
accuracy = row[:horizontal_accuracy].split
|
74
|
+
obstacle.xy_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
|
75
|
+
end
|
76
|
+
if row[:vertical_accuracy]
|
77
|
+
accuracy = row[:horizontal_accuracy].split
|
78
|
+
obstacle.z_accuracy = AIXM.d(accuracy.first.to_i, accuracy.last)
|
79
|
+
end
|
80
|
+
obstacle.marking = row[:visibility].match?(/jour/i)
|
81
|
+
obstacle.lighting = row[:visibility].match?(/nuit/i)
|
82
|
+
obstacle.remarks = {
|
83
|
+
'type' => type_remarks,
|
84
|
+
'number/nombre' => (count if count > 1),
|
85
|
+
'details' => row[:remarks],
|
86
|
+
'effective/mise en vigueur' => (row[:effective_on].to_s.unpack("a4a2a2").join("-") if row[:updated_on])
|
87
|
+
}.to_remarks
|
88
|
+
# Group obstacles
|
89
|
+
if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
|
90
|
+
warn("duplicate obstacle #{obstacle.name}", severe: false)
|
91
|
+
else
|
92
|
+
if count > 1
|
93
|
+
obstacle_group = AIXM.obstacle_group(
|
94
|
+
source: obstacle.source,
|
95
|
+
name: obstacle.name
|
96
|
+
).tap do |obstacle_group|
|
97
|
+
obstacle_group.remarks = "#{count} obstacles"
|
98
|
+
end
|
99
|
+
obstacle_group.add_obstacle obstacle
|
100
|
+
add obstacle_group
|
101
|
+
else
|
102
|
+
add obstacle
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_from_xml
|
110
|
+
AIPP.cache.obstacle.css(%Q(Obstacle[lk^="[LF]"])).each do |node|
|
111
|
+
# Build obstacles
|
112
|
+
type, type_remarks = TYPES.fetch(node.(:TypeObst))
|
113
|
+
count = node.(:Combien).to_i
|
114
|
+
obstacle = AIXM.obstacle(
|
115
|
+
source: source(part: 'ENR', position: node.line),
|
116
|
+
name: node.(:NumeroNom),
|
117
|
+
type: type,
|
118
|
+
xy: xy_from(node.(:Geometrie)),
|
119
|
+
z: AIXM.z(node.(:AmslFt).to_i, :qnh)
|
120
|
+
).tap do |obstacle|
|
121
|
+
obstacle.height = AIXM.d(node.(:AglFt).to_i, :ft)
|
122
|
+
obstacle.marking = node.(:Balisage).match?(/jour/i)
|
123
|
+
obstacle.lighting = node.(:Balisage).match?(/nuit/i)
|
124
|
+
obstacle.remarks = {
|
125
|
+
'type' => type_remarks,
|
126
|
+
'number/nombre' => (count if count > 1)
|
127
|
+
}.to_remarks
|
128
|
+
end
|
129
|
+
# Group obstacles
|
130
|
+
if aixm.features.find_by(:obstacle, xy: obstacle.xy).any?
|
131
|
+
warn("duplicate obstacle #{obstacle.name}", severe: false)
|
132
|
+
else
|
133
|
+
if count > 1
|
134
|
+
obstacle_group = AIXM.obstacle_group(
|
135
|
+
source: obstacle.source,
|
136
|
+
name: obstacle.name
|
137
|
+
).tap do |obstacle_group|
|
138
|
+
obstacle_group.remarks = "#{count} obstacles"
|
139
|
+
end
|
140
|
+
obstacle_group.add_obstacle obstacle
|
141
|
+
add obstacle_group
|
142
|
+
else
|
143
|
+
add obstacle
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module AIPP::LF::AIP
|
2
|
+
class ServicedAirspaces < AIPP::AIP::Parser
|
3
|
+
|
4
|
+
include AIPP::LF::Helpers::Base
|
5
|
+
|
6
|
+
# Map source types to type and optional local type and skip regexp
|
7
|
+
SOURCE_TYPES = {
|
8
|
+
'FIR' => { type: 'FIR' },
|
9
|
+
'UIR' => { type: 'UIR' },
|
10
|
+
'UTA' => { type: 'UTA' },
|
11
|
+
'CTA' => { type: 'CTA' },
|
12
|
+
'LTA' => { type: 'CTA', local_type: 'LTA' },
|
13
|
+
'TMA' => { type: 'TMA', skip: /geneve/i }, # Geneva listed FYI only
|
14
|
+
'SIV' => { type: 'SECTOR', local_type: 'FIZ/SIV' }, # providing FIS
|
15
|
+
'CTR' => { type: 'CTR' },
|
16
|
+
'RMZ' => { type: 'RAS', local_type: 'RMZ' },
|
17
|
+
'TMZ' => { type: 'RAS', local_type: 'TMZ' },
|
18
|
+
'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Map airspace "<type> <name>" to location indicator
|
22
|
+
FIR_LOCATION_INDICATORS = {
|
23
|
+
'BORDEAUX' => 'LFBB',
|
24
|
+
'BREST' => 'LFRR',
|
25
|
+
'MARSEILLE' => 'LFMM',
|
26
|
+
'PARIS' => 'LFFF',
|
27
|
+
'REIMS' => 'LFRR'
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
def parse
|
31
|
+
SOURCE_TYPES.each do |source_type, target|
|
32
|
+
verbose_info("processing #{source_type}")
|
33
|
+
AIPP.cache.espace.css(%Q(Espace[lk^="[LF][#{source_type} "])).each do |espace_node|
|
34
|
+
# Skip all delegated airspaces
|
35
|
+
next if espace_node.(:Nom).match? /deleg/i
|
36
|
+
next if (re = target[:skip]) && espace_node.(:Nom).match?(re)
|
37
|
+
# Build airspaces and layers
|
38
|
+
AIPP.cache.partie.css(%Q(Partie:has(Espace[pk="#{espace_node['pk']}"]))).each do |partie_node|
|
39
|
+
add(
|
40
|
+
AIXM.airspace(
|
41
|
+
source: source(part: 'ENR', position: espace_node.line),
|
42
|
+
name: [
|
43
|
+
espace_node.(:Nom),
|
44
|
+
partie_node.(:NomPartie).remove(/^\.$/).blank_to_nil
|
45
|
+
].compact.join(' '),
|
46
|
+
type: target[:type],
|
47
|
+
local_type: target[:local_type]
|
48
|
+
).tap do |airspace|
|
49
|
+
airspace.meta = espace_node.attr('pk')
|
50
|
+
airspace.geometry = geometry_from(partie_node.(:Contour))
|
51
|
+
fail("geometry is not closed") unless airspace.geometry.closed?
|
52
|
+
AIPP.cache.volume.css(%Q(Volume:has(Partie[pk="#{partie_node['pk']}"]))).each do |volume_node|
|
53
|
+
airspace.add_layer(
|
54
|
+
layer_from(volume_node).tap do |layer|
|
55
|
+
layer.location_indicator = FIR_LOCATION_INDICATORS.fetch(airspace.name) if airspace.type == :flight_information_region
|
56
|
+
end
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module AIPP::LF::AIP
|
2
|
+
class Services < AIPP::AIP::Parser
|
3
|
+
|
4
|
+
include AIPP::LF::Helpers::Base
|
5
|
+
|
6
|
+
depends_on :Aerodromes, :ServicedAirspaces
|
7
|
+
|
8
|
+
# Service types and how to treat them
|
9
|
+
SOURCE_TYPES = {
|
10
|
+
'A/A' => :address,
|
11
|
+
'AFIS' => :service,
|
12
|
+
'APP' => :service,
|
13
|
+
'ATIS' => :service,
|
14
|
+
'CCM' => :ignore, # centre de contrôle militaire
|
15
|
+
'CEV' => :ignore, # centre d’essai en vol
|
16
|
+
'D-ATIS' => :ignore, # data link ATIS
|
17
|
+
'FIS' => :service,
|
18
|
+
'PAR' => :service,
|
19
|
+
'SRE' => :ignore, # surveillance radar element of PAR
|
20
|
+
'TWR' => :service,
|
21
|
+
'UAC' => :ignore, # upper area control
|
22
|
+
'VDF' => :service,
|
23
|
+
'OTHER' => :service # no <Service> specified at source
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
# Map French callsigns to English and service type
|
27
|
+
CALLSIGNS = {
|
28
|
+
'Approche' => { en: 'Approach', service_type: 'APP' },
|
29
|
+
'Contrôle' => { en: 'Control', service_type: 'ACS' },
|
30
|
+
'Information' => { en: 'Information', service_type: 'FIS' },
|
31
|
+
'GCA' => { en: 'GCA', service_type: 'GCA' },
|
32
|
+
'Gonio' => { en: 'Gonio', service_type: 'VDF' },
|
33
|
+
'Prévol' => { en: 'Delivery', service_type: 'SMC' },
|
34
|
+
'Sol' => { en: 'Ground', service_type: 'SMC' },
|
35
|
+
'Tour' => { en: 'Tower', service_type: 'TWR' },
|
36
|
+
'Trafic' => { en: 'Apron', service_type: 'SMC' }
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
def parse
|
40
|
+
AIPP.cache.service.css(%Q(Service[lk^="[LF]"][pk])).each do |service_node|
|
41
|
+
# Ignore private services/frequencies
|
42
|
+
next if service_node.(:IndicLieu) == 'XX'
|
43
|
+
# Load referenced airport
|
44
|
+
airport = given(service_node.at_css('Ad')&.attr('pk')) do
|
45
|
+
find_by(:airport, meta: _1).first
|
46
|
+
end
|
47
|
+
# Build addresses and services
|
48
|
+
case SOURCE_TYPES.fetch(type_for(service_node))
|
49
|
+
when :address
|
50
|
+
fail "dangling address without airport" unless airport
|
51
|
+
addresses_from(service_node).each { airport.add_address(_1) }
|
52
|
+
when :service
|
53
|
+
given service_from(service_node) do |service|
|
54
|
+
AIPP.cache.frequence.css(%Q(Frequence:has(Service[pk="#{service_node['pk']}"]))).each do |frequence_node|
|
55
|
+
if frequency = frequency_from(frequence_node, service_node)
|
56
|
+
service.add_frequency frequency
|
57
|
+
end
|
58
|
+
end
|
59
|
+
if airport
|
60
|
+
airport.add_unit(service.unit) if airport.units.find(service.unit).none?
|
61
|
+
airport.add_service(service) if airport.services.find(service).none?
|
62
|
+
end
|
63
|
+
given service_node.at_css('Espace')&.attr('pk') do |espace_pk|
|
64
|
+
find_by(:airspace, meta: espace_pk).each do |airspace|
|
65
|
+
airspace.layers.each { _1.add_service(service) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
# Assign fallback address (default A/A frequency) to all yet radioless airports
|
72
|
+
find_by(:airport).each do |airport|
|
73
|
+
unless airport.services.find_by(:service, type: :aerodrome_control_tower_service).any? || airport.addresses.any?
|
74
|
+
airport.add_address(fallback_address_for(airport.name))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def type_for(service_node)
|
82
|
+
SOURCE_TYPES.include?(type = service_node.(:Service)) ? type : 'OTHER'
|
83
|
+
end
|
84
|
+
|
85
|
+
def addresses_from(service_node)
|
86
|
+
AIPP.cache.frequence.css(%Q(Frequence:has(Service[pk="#{service_node['pk']}"]))).map do |frequence_node|
|
87
|
+
if frequency = frequency_from(frequence_node, service_node)
|
88
|
+
AIXM.address(
|
89
|
+
type: :radio_frequency,
|
90
|
+
address: frequency.transmission_f
|
91
|
+
).tap do |address|
|
92
|
+
address.remarks = {
|
93
|
+
'type' => service_node.(:Service),
|
94
|
+
'indicatif/callsign' => frequency.callsigns.map { "#{_2} (#{_1})" }.join("/n")
|
95
|
+
}.to_remarks
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end.compact
|
99
|
+
end
|
100
|
+
|
101
|
+
def fallback_address_for(callsign)
|
102
|
+
AIXM.address(
|
103
|
+
type: :radio_frequency,
|
104
|
+
address: AIXM.f(123.5, :mhz)
|
105
|
+
).tap do |address|
|
106
|
+
address.remarks = {
|
107
|
+
'type' => 'A/A',
|
108
|
+
'indicatif/callsign' => callsign
|
109
|
+
}.to_remarks
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def service_from(service_node)
|
114
|
+
service_type = CALLSIGNS.dig(service_node.(:IndicService), :service_type) || type_for(service_node)
|
115
|
+
service = find_by(:service, type: service_type).first
|
116
|
+
unit = service&.unit
|
117
|
+
unless service
|
118
|
+
service = AIXM.service(type: service_type)
|
119
|
+
unit = find_by(:unit, name: service_node.(:IndicLieu), type: service.guessed_unit_type).first
|
120
|
+
unless unit
|
121
|
+
unit = AIXM.unit(
|
122
|
+
source: source(part: 'GEN', position: service_node.line),
|
123
|
+
organisation: organisation_lf,
|
124
|
+
name: service_node.(:IndicLieu),
|
125
|
+
type: service.guessed_unit_type,
|
126
|
+
class: :icao
|
127
|
+
)
|
128
|
+
add unit
|
129
|
+
end
|
130
|
+
unit.add_service service
|
131
|
+
service
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def frequency_from(frequence_node, service_node)
|
136
|
+
frequency = frequence_node.(:Frequence).to_f
|
137
|
+
case
|
138
|
+
when frequency >= 137
|
139
|
+
nil
|
140
|
+
when frequency < 108
|
141
|
+
warn("ignoring too low frequency `#{frequency}'", severe: false)
|
142
|
+
nil
|
143
|
+
else
|
144
|
+
AIXM.frequency(
|
145
|
+
transmission_f: AIXM.f(frequency, :mhz),
|
146
|
+
callsigns: callsigns_from(service_node)
|
147
|
+
).tap do |frequency|
|
148
|
+
frequency.timetable = timetable_from(frequence_node.(:HorCode))
|
149
|
+
frequency.remarks = frequence_node.(:Remarque)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def callsigns_from(service_node)
|
155
|
+
if service_node.(:IndicService) == '.' # auto-information
|
156
|
+
%i(fr en).to_h { [_1, '(auto)'] }
|
157
|
+
else
|
158
|
+
warn("language other than french") unless service_node.(:Langue) == 'fr'
|
159
|
+
english = CALLSIGNS.fetch(service_node.(:IndicService)).fetch(:en)
|
160
|
+
warn("no english translation for callsign `#{service_node.(:IndicService)}'") unless english
|
161
|
+
{
|
162
|
+
fr: "#{service_node.(:IndicLieu)} #{service_node.(:IndicService)}",
|
163
|
+
en: ("#{service_node.(:IndicLieu)} #{english}" if english)
|
164
|
+
}.compact
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
@@ -12,47 +12,47 @@ module AIPP
|
|
12
12
|
|
13
13
|
def setup
|
14
14
|
AIXM.config.voice_channel_separation = :any
|
15
|
-
unless cache.espace
|
15
|
+
unless AIPP.cache.espace
|
16
16
|
xml = read('XML_SIA')
|
17
|
-
%i(Ad Bordure Espace Frequence Helistation NavFix Obstacle Partie RadioNav Rwy RwyLgt Service Volume).each do |
|
18
|
-
cache[
|
17
|
+
%i(Ad Bordure Espace Frequence Helistation NavFix Obstacle Partie RadioNav Rwy RwyLgt Service Volume).each do |table|
|
18
|
+
AIPP.cache[table.downcase] = xml.css("#{table}S")
|
19
19
|
end
|
20
20
|
warn("XML_SIA database dump version mismatch") unless xml.at_css('SiaExport').attr(:Version) == VERSION
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
sia_date = options
|
26
|
-
xml_date = options
|
24
|
+
def origin_for(document)
|
25
|
+
sia_date = AIPP.options.airac.date.strftime('%d_%^b_%Y') # 04_JAN_2018
|
26
|
+
xml_date = AIPP.options.airac.date.xmlschema # 2018-01-04
|
27
27
|
sia_url = "https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_#{sia_date}"
|
28
|
-
case
|
28
|
+
case document
|
29
29
|
when /^Obstacles$/ # obstacles spreadsheet
|
30
|
-
"#{sia_url}/FRANCE/ObstaclesDataZone1MFRANCE_#{xml_date.remove('-')}.xlsx"
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
AIPP::Downloader::HTTP.new(file: "#{sia_url}/FRANCE/ObstaclesDataZone1MFRANCE_#{xml_date.remove('-')}.xlsx")
|
31
|
+
# when /^VAC\-(\w+)/ # aerodrome VAC PDF
|
32
|
+
# AIPP::Downloader::HTTP.new(file: "#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.#{$1}.pdf")
|
33
|
+
# when /^VACH\-(\w+)/ # helipad VAC PDF
|
34
|
+
# AIPP::Downloader::HTTP.new(file: "#{sia_url}/Atlas-VAC/PDF_AIPparSSection/VACH/AD/AD-3.#{$1}.pdf")
|
35
|
+
# when /^[A-Z]+-/ # eAIP HTML page (e.g. ENR-5.5)
|
36
|
+
# AIPP::Downloader::HTTP.new(file: "#{sia_url}/FRANCE/AIRAC-#{xml_date}/html/eAIP/FR-#{document}-fr-FR.html")
|
37
37
|
else # SIA XML database dump
|
38
|
-
"XML_SIA_#{xml_date}.xml"
|
38
|
+
AIPP::Downloader::File.new(file: "XML_SIA_#{xml_date}.xml")
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
# Templates
|
43
43
|
|
44
44
|
def organisation_lf
|
45
|
-
unless cache.organisation_lf
|
46
|
-
cache.organisation_lf = AIXM.organisation(
|
47
|
-
source: source(position: 1,
|
45
|
+
unless AIPP.cache.organisation_lf
|
46
|
+
AIPP.cache.organisation_lf = AIXM.organisation(
|
47
|
+
source: source(position: 1, document: "GEN-3.1"),
|
48
48
|
name: 'FRANCE',
|
49
49
|
type: 'S'
|
50
50
|
).tap do |organisation|
|
51
51
|
organisation.id = 'LF'
|
52
52
|
end
|
53
|
-
add cache.organisation_lf
|
53
|
+
add AIPP.cache.organisation_lf
|
54
54
|
end
|
55
|
-
cache.organisation_lf
|
55
|
+
AIPP.cache.organisation_lf
|
56
56
|
end
|
57
57
|
|
58
58
|
# Parsersettes
|
@@ -60,17 +60,17 @@ module AIPP
|
|
60
60
|
# Build a source string
|
61
61
|
#
|
62
62
|
# @param position [Integer] line on which to find the information
|
63
|
-
# @param
|
64
|
-
# @param
|
63
|
+
# @param part [String] override autodetected part (e.g. "ENR")
|
64
|
+
# @param document [String] override autodetected document (e.g. "ENR-2.1")
|
65
65
|
# @return [String] source string
|
66
|
-
def source(position:,
|
67
|
-
|
68
|
-
|
66
|
+
def source(position:, part: nil, document: nil)
|
67
|
+
document ||= 'XML_SIA'
|
68
|
+
part ||= document.split(/-(?=\d)/).first
|
69
69
|
[
|
70
|
-
options
|
71
|
-
|
72
|
-
|
73
|
-
options
|
70
|
+
AIPP.options.region,
|
71
|
+
part,
|
72
|
+
document,
|
73
|
+
AIPP.options.airac.date.xmlschema,
|
74
74
|
position
|
75
75
|
].join('|')
|
76
76
|
end
|
@@ -134,7 +134,7 @@ module AIPP
|
|
134
134
|
parts = element.split(',', 3).last.split(/[():,]/)
|
135
135
|
# Write explicit geometry from previous iteration
|
136
136
|
if (bordure_name, xy = buffer.delete(:fnt))
|
137
|
-
border = borders
|
137
|
+
border = AIPP.borders.send(bordure_name)
|
138
138
|
geometry.add_segments border.segment(
|
139
139
|
from_position: border.nearest(xy: xy),
|
140
140
|
to_position: border.nearest(xy: xy_from(parts[0]))
|
@@ -163,10 +163,10 @@ module AIPP
|
|
163
163
|
radius: d_from(parts[3..4].join(' '))
|
164
164
|
)
|
165
165
|
when 'fnt'
|
166
|
-
bordure = cache.bordure.at_css(%Q(Bordure[pk="#{parts[3]}"]))
|
166
|
+
bordure = AIPP.cache.bordure.at_css(%Q(Bordure[pk="#{parts[3]}"]))
|
167
167
|
bordure_name = bordure.(:Code)
|
168
168
|
if bordure_name.match? /:/ # explicit geometry
|
169
|
-
borders[bordure_name] ||= AIPP::Border.from_array([bordure.(:Geometrie).split])
|
169
|
+
AIPP.borders[bordure_name] ||= AIPP::Border.from_array([bordure.(:Geometrie).split])
|
170
170
|
buffer[:fnt] = [bordure_name, xy_from(parts[2])]
|
171
171
|
AIXM.point(
|
172
172
|
xy: xy_from(parts[0])
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# LS – Switzerland
|
2
|
+
|
3
|
+
## Neway API
|
4
|
+
|
5
|
+
The NOTAM messages are fetched from the Neway API which is a non-public
|
6
|
+
GraphQL API.
|
7
|
+
|
8
|
+
You have to set the following environment variables:
|
9
|
+
|
10
|
+
* `NEWAY_API_URL` – API endpoint
|
11
|
+
* `NEWAY_API_AUTHORIZATION` – the bearer authentication token
|
12
|
+
|
13
|
+
The following query object shows all parameters and columns:
|
14
|
+
|
15
|
+
```
|
16
|
+
{
|
17
|
+
queryNOTAMs(
|
18
|
+
filter: {
|
19
|
+
country: "CHE", # country as detected by ICAO
|
20
|
+
series: ["W", "B"], # NOTAM series (first letter of name)
|
21
|
+
region: "LS", # FIR region (extracted from name)
|
22
|
+
start: 1651449600, # time window begins (UTC timestamp)
|
23
|
+
end: 1651535999 # time window ends (UTC timestamp)
|
24
|
+
}
|
25
|
+
) {
|
26
|
+
id # internal ID
|
27
|
+
name # NOTAM name
|
28
|
+
notamRaw # raw NOTAM message
|
29
|
+
series # NOTAM series (first letter of name)
|
30
|
+
region # FIR region (extracted from name)
|
31
|
+
country # country as detected by ICAO
|
32
|
+
area # NOTAM topic area
|
33
|
+
effectiveFrom # validity begins (UTC timestamp)
|
34
|
+
validUntil # validity ends (UTC timestamp)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
```
|
38
|
+
|
39
|
+
## Asynchronous Use
|
40
|
+
|
41
|
+
### Command Line Arguments
|
42
|
+
|
43
|
+
When used asynchronously, the following command line arguments should be considered:
|
44
|
+
|
45
|
+
* `-c` – clear cache (mandatory to create builds more often than once per hour)
|
46
|
+
* `-f` – continue on non-fatal errors such as if the validation fails
|
47
|
+
* `-q` – suppress all informational output
|
48
|
+
* `-x` – crossload fixed versions of malformed NOTAM
|
49
|
+
|
50
|
+
### Monitoring
|
51
|
+
|
52
|
+
Any output on STDERR should trigger an alert.
|
53
|
+
|
54
|
+
## References
|
55
|
+
|
56
|
+
* [skybriefing](https://www.skybriefing.com)
|
57
|
+
* [AIM Data Catalogue](https://www.aerodatacat.ch)
|
58
|
+
* [DABS](https://www.skybriefing.com/de/dabs)
|
59
|
+
* [NOTAM Info](https://notaminfo.com/switzerlandmap)
|