using AIXM::Refinements module AIXM class Feature # Defined area on land or water to be used for the arrival, departure and # surface movement of aircraft. # # ===Cheat Sheet in Pseudo Code: # airport = AIXM.airport( # source: String or nil # region: String or nil # organisation: AIXM.organisation # id: String # name: String # xy: AIXM.xy # ) # airport.gps = String or nil # airport.type = TYPES (other than AD, HP and AH only) # airport.z = AIXM.z or nil # airport.declination = Float or nil # airport.transition_z = AIXM.z or nil # airport.timetable = AIXM.timetable or nil # airport.operator = String or nil # airport.remarks = String or nil # airport.comment = Object or nil # airport.add_runway(AIXM.runway) # airport.add_fato(AIXM.fato) # airport.add_helipad(AIXM.helipad) # airport.add_usage_limitation(UsageLimitation::TYPES) # airport.add_unit(AIXM.unit) # airport.add_service(AIXM.service) # airport.add_address(AIXM.address) # # For airports without an +id+, you may assign the two character region # (e.g. "LF") which will be combined with an 8 character digest of +name+. # # @see https://gitlab.com/openflightmaps/ofmx/wikis/Airport#ahp-airport class Airport < Feature include AIXM::Concerns::Association include AIXM::Concerns::Timetable include AIXM::Concerns::Remarks public_class_method :new ID_RE = /^([A-Z]{3,4}|[A-Z]{2}[A-Z\d]{4,})$/.freeze TYPES = { AD: :aerodrome, HP: :heliport, AH: :aerodrome_and_heliport, LS: :landing_site }.freeze # @!method fatos # @return [Array] FATOs present at this airport # # @!method add_fato(fato) # @param fato [AIXM::Component::FATO] has_many :fatos # @!method helipads # @return [Array] helipads present at this airport # # @!method add_helipad(helipad) # @param helipad [AIXM::Component::Helipad] has_many :helipads # @!method runways # @return [Array] runways present at this airport # # @!method add_runway(runway) # @param runway [AIXM::Component::Runway] has_many :runways # @!method usage_limitations # @return [Array] usage limitations # # @!method add_usage_limitation # @yield [AIXM::Feature::Airport::UsageLimitation] # @return [self] has_many :usage_limitations, accept: 'AIXM::Feature::Airport::UsageLimitation' do |usage_limitation, type:| end # @!method designated_points # @return [Array] designated points # # @!method add_designated_point(designated_point) # @param designated_point [AIXM::Feature::NavigationalAid::DesignatedPoint] has_many :designated_points # @!method units # @return [Array] units # # @!method add_unit(unit) # @param unit [AIXM::Feature::Unit] has_many :units # @!method services # @return [Array] services # # @!method add_service(service) # @param service [AIXM::Component::Service] has_many :services # @!method addresses # @return [Array] postal address, url, A/A or A/G frequency etc # # @!method add_address(address) # @param address [AIXM::Feature::Address] # @return [self] has_many :addresses, as: :addressable # @!method organisation # @return [AIXM::Feature::Organisation] superior organisation belongs_to :organisation, as: :member # ICAO, IATA or generated airport indicator. # # * four letter ICAO indicator (e.g. "LFMV") # * three letter IATA indicator (e.g. "AVN") # * two letter ICAO country code + four digit number (e.g. "LF1234") # * two letter ICAO country code + at least four letters/digits (e.g. # "LFFOOBAR123" or "LF" + GPS code) # # @overload id # @return [String] # @overload id=(value) # @param value [String] attr_reader :id # Full name # # @overload name # @return [String] # @overload name=(value) # @param value [String] attr_reader :name # Reference point # # @overload xy # @return [AIXM::XY] # @overload xy=(value) # @param value [AIXM::XY] attr_reader :xy # GPS code # # @overload gps # @return [String, nil] # @overload gps=(value) # @param value [String, nil] attr_reader :gps # Elevation in +:qnh+ # # @overload z # @return [AIXM::Z, nil] # @overload z=(value) # @param value [AIXM::Z, nil] attr_reader :z # When looking towards the geographic (aka: true) north, a positive # declination represents the magnetic north is to the right (aka: east) # by this angle. # # To convert a magnetic bearing to the corresponding geographic (aka: # true) bearing, the declination has to be added. # # @see https://en.wikipedia.org/wiki/Magnetic_declination # @return [Float, nil] magnetic declination in degrees attr_reader :declination # Transition altitude in +:qnh+ # # @overload transition_z # @return [AIXM::Z, nil] # @overload transition_z=(value) # @param value [AIXM::Z, nil] attr_reader :transition_z # Operator of the airport # # @overload operator # @return [String, nil] # @overload operator=(value) # @param value [String, nil] attr_reader :operator # See the {cheat sheet}[AIXM::Feature::Airport] for examples on how to # create instances of this class. def initialize(source: nil, region: nil, organisation:, id: nil, name:, xy:) super(source: source, region: region) self.organisation, self.name, self.id, self.xy = organisation, name, id, xy # name must be set before id end # @return [String] def inspect %Q(#<#{self.class} id=#{id.inspect}>) end # For airports without an +id+, you may assign the two character region # (e.g. "LF") which will be combined with an 8 character digest of +name+. def id=(value) value = [value, [name].to_digest].join.upcase if value&.upcase&.match? AIXM::Feature::REGION_RE fail(ArgumentError, "invalid id") unless value&.upcase&.match? ID_RE @id = value.upcase end def name=(value) fail(ArgumentError, "invalid name") unless value.is_a? String @name = value.uptrans end def gps=(value) fail(ArgumentError, "invalid gps") unless value.nil? || value.is_a?(String) @gps = value&.upcase end # Type of airport. # # The type is usually derived from the presence of runways and helipads, # however, this may be overridden by setting an alternative value, most # notably +:landing_site+. # # @!attribute type # @overload type # @return [Symbol] any of {TYPES} # @overload type=(value) # @param value [Symbol] any of {TYPES} def type @type = case when @type then @type when runways.any? && (helipads.any? || fatos.any?) then :aerodrome_and_heliport when runways.any? then :aerodrome when helipads.any? || fatos.any? then :heliport end end def type=(value) resolved_value = TYPES.lookup(value&.to_s&.to_sym, nil) fail(ArgumentError, "invalid type") unless resolved_value == :landing_site @type = resolved_value end def xy=(value) fail(ArgumentError, "invalid xy") unless value.is_a? AIXM::XY @xy = value end def z=(value) fail(ArgumentError, "invalid z") unless value.nil? || (value.is_a?(AIXM::Z) && value.qnh?) @z = value end def declination=(value) return @declination = value if value.nil? fail(ArgumentError, "invalid declination") unless value.is_a?(Numeric) && (-180..180).include?(value) @declination = value.to_f + 0 # adding zero prevents -0.0 end def transition_z=(value) fail(ArgumentError, "invalid transition_z") unless value.nil? || (value.is_a?(AIXM::Z) && value.qnh?) @transition_z = value end def operator=(value) fail(ArgumentError, "invalid name") unless value.nil? || value.is_a?(String) @operator = value&.uptrans end # @!visibility private def add_uid_to(builder, as: :AhpUid) builder.send(as, { region: (region if AIXM.ofmx?) }.compact) do |tag| tag.codeId(id) end end # @!visibility private def add_wrapped_uid_to(builder, as: :AhpUid, with:) builder.send(with) do |tag| add_uid_to(builder, as: as) end end # @!visibility private def add_to(builder) builder.comment "Airport: #{id} #{name}".dress builder.text "\n" builder.Ahp({ source: (source if AIXM.ofmx?) }.compact) do |ahp| ahp.comment(indented_comment) if comment add_uid_to(ahp) organisation.add_uid_to(ahp) ahp.txtName(name) ahp.codeIcao(id) if id.length == 4 ahp.codeIata(id) if id.length == 3 ahp.codeGps(gps) if AIXM.ofmx? && gps ahp.codeType(TYPES.key(type)) if type ahp.geoLat(xy.lat(AIXM.schema)) ahp.geoLong(xy.long(AIXM.schema)) ahp.codeDatum('WGE') if z ahp.valElev(z.alt) ahp.uomDistVer(z.unit.upcase) end ahp.valMagVar(declination) if declination ahp.txtNameAdmin(operator) if operator if transition_z ahp.valTransitionAlt(transition_z.alt) ahp.uomTransitionAlt(transition_z.unit.upcase) end timetable.add_to(ahp, as: :Aht) if timetable ahp.txtRmk(remarks) if remarks end runways.each do |runway| runway.add_to(builder) end fatos.each do |fato| fato.add_to(builder) end helipads.each do |helipad| helipad.add_to(builder) end if usage_limitations.any? builder.Ahu do |ahu| add_wrapped_uid_to(ahu, with: :AhuUid) usage_limitations.each do |usage_limitation| usage_limitation.add_to(ahu) end end end addresses.each.with_object({}) do |address, sequences| sequences[address.type] = (sequences[address.type] || 0) + 1 address.add_to(builder, as: :Aha, sequence: sequences[address.type]) end services.each do |service| builder.Sah do |sah| sah.SahUid do |sah_uid| add_uid_to(sah_uid) service.add_uid_to(sah_uid) end end end end # Limitations concerning the availability of an airport for certain flight # types, aircraft types etc during specific hours. # # See {AIXM::Feature::Airport::UsageLimitation::TYPES UsageLimitation::TYPES} # for recognized limitations and {AIXM::Feature::Airport::UsageLimitation#add_condition UsageLimitation#add_condition} # for recognized conditions. # # Multiple conditions are joined with an implicit *or* whereas the # specifics of a condition (aircraft, rule etc) are joined with an # implicit *and*. # # @example Limitation applying to any traffic # airport.add_usage_limitation(type: :permitted) # # @example Limitation applying to specific traffic # airport.add_usage_limitation(type: :reservation_required) do |reservation_required| # reservation_required.add_condition do |condition| # condition.aircraft = :glider # end # reservation_required.add_condition do |condition| # condition.rule = :ifr # condition.origin = :international # end # reservation_required.timetable = AIXM::H24 # reservation_required.remarks = "Reservation 24 HRS prior to arrival" # end # # @see AIXM::Feature::Airport#add_usage_limitation # @see https://gitlab.com/openflightmaps/ofmx/wikis/Airport#ahu-airport-usage class UsageLimitation include AIXM::Concerns::Association include AIXM::Concerns::XMLBuilder include AIXM::Concerns::Timetable include AIXM::Concerns::Remarks TYPES = { PERMIT: :permitted, FORBID: :forbidden, RESERV: :reservation_required, OTHER: :other # specify in remarks }.freeze # @!method conditions # @return [Array] conditions for this limitation to apply # # @!method add_condition # @yield [AIXM::Feature::Airport::UsageLimitation::Condition] # @return [self] has_many :conditions, accept: 'AIXM::Feature::Airport::UsageLimitation::Condition' do |condition| end # @!method airport # @return [AIXM::Feature::Airport] airport this usage limitation is assigned to belongs_to :airport # Type of limitation # # @overload type # @return [Symbol] any of {TYPES} # @overload type=(value) # @param value [Symbol] any of {TYPES} attr_reader :type # See the {cheat sheet}[AIXM::Feature::Airport::UsageLimitation] for # examples on how to create instances of this class. def initialize(type:) self.type = type end # @return [String] def inspect %Q(#<#{self.class} type=#{type.inspect}>) end def type=(value) @type = TYPES.lookup(value&.to_s&.to_sym, nil) || fail(ArgumentError, "invalid type") end # @!visibility private def add_to(builder) builder.UsageLimitation do |usage_limitation| usage_limitation.codeUsageLimitation(TYPES.key(type)) conditions.each do |condition| condition.add_to(usage_limitation) end timetable.add_to(usage_limitation, as: :Timetable) if timetable usage_limitation.txtRmk(remarks) if remarks end end # Flight and/or aircraft characteristics used to target a usage # limitation. # # @see AIXM::Feature::Airport#add_usage_limitation # @see https://gitlab.com/openflightmaps/ofmx/wikis/Airport#ahu-airport-usage class Condition include AIXM::Concerns::Association include AIXM::Concerns::XMLBuilder AIRCRAFT = { L: :landplane, S: :seaplane, A: :amphibian, H: :helicopter, G: :gyrocopter, T: :tilt_wing, R: :short_takeoff_and_landing, E: :glider, N: :hangglider, P: :paraglider, U: :ultra_light, B: :balloon, D: :unmanned_drone, OTHER: :other # specify in remarks }.freeze RULES = { I: :ifr, V: :vfr, IV: :ifr_and_vfr }.freeze REALMS = { CIVIL: :civilian, MIL: :military, OTHER: :other # specify in remarks }.freeze ORIGINS = { NTL: :national, INTL: :international, ANY: :any, OTHER: :other # specify in remarks }.freeze PURPOSES = { S: :scheduled, NS: :not_scheduled, P: :private, TRG: :school_or_training, WORK: :aerial_work, OTHER: :other # specify in remarks }.freeze # @!method usage_limitation # @return [AIXM::Feature::Airport::UsageLimitation] usage limitation the condition belongs to belongs_to :usage_limitation # Kind of aircraft. # # @overload aircraft # @return [Symbol, nil] any of {AIRCRAFT} # @overload aircraft=(value) # @param value [Symbol, nil] any of {AIRCRAFT} attr_reader :aircraft # Flight rule. # # @overload rule # @return [Symbol, nil] any of {RULES} # @overload rule=(value) # @param value [Symbol, nil] any of {RULES} attr_reader :rule # Military, civil etc. # # @overload realm # @return [Symbol, nil] any of {REALMS} # @overload realm=(value) # @param value [Symbol, nil] any of {REALMS} attr_reader :realm # Geographic origin of the flight. # # @overload origin # @return [Symbol, nil] any of {ORIGINS} # @overload origin=(value) # @param value [Symbol, nil] any of {ORIGINS} attr_reader :origin # Purpose of the flight. # # @overload purpose # @return [Symbol, nil] any of {PURPOSES} # @overload purpose=(value) # @param value [Symbol, nil] any of {PURPOSES} attr_reader :purpose # @return [String] def inspect %Q(#<#{self.class} aircraft=#{aircraft.inspect} rule=#{rule.inspect} realm=#{realm.inspect} origin=#{origin.inspect} purpose=#{purpose.inspect}>) end def aircraft=(value) @aircraft = value.nil? ? nil : AIRCRAFT.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid aircraft") end def rule=(value) @rule = value.nil? ? nil : RULES.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid rule") end def realm=(value) @realm = value.nil? ? nil : REALMS.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid realm") end def origin=(value) @origin = value.nil? ? nil : ORIGINS.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid origin") end def purpose=(value) @purpose = value.nil? ? nil : PURPOSES.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid purpose") end # @!visibility private def add_to(builder) builder.UsageCondition do |usage_condition| if aircraft usage_condition.AircraftClass do |aircraft_class| aircraft_class.codeType(AIRCRAFT.key(aircraft)) end end if rule || realm || origin || purpose usage_condition.FlightClass do |flight_class| flight_class.codeRule(RULES.key(rule)) if rule flight_class.codeMil(REALMS.key(realm)) if realm flight_class.codeOrigin(ORIGINS.key(origin)) if origin flight_class.codePurpose(PURPOSES.key(purpose)) if purpose end end end end end end end end end