lib/aixm/feature/airspace.rb

Summary

Maintainability
A
1 hr
Test Coverage
using AIXM::Refinements

module AIXM
  class Feature

    # Three-dimensional volume most notably defining flight zones.
    #
    # ===Cheat Sheet in Pseudo Code:
    #   airspace = AIXM.airspace(
    #     source: String or nil
    #     region: String or nil
    #     id: String or nil   # nil is converted to an 8 character digest
    #     type: String or Symbol
    #     local_type: String or nil
    #     name: String or nil
    #   )
    #   airspace.alternative_name = String (OFMX only)
    #   airspace.comment = Object or nil
    #   airspace.add_layer(AIXM.layer)
    #   airspace.geometry.add_segment(AIXM.point or AIXM.arc or AIXM.border or AIXM.circle)
    #
    # Timetables and remarks have to be set on the layer!
    #
    # The +id+ is mandatory, however, you may omit it when initializing a new
    # airspace or assign +nil+ to an existing airspace which will generate a 8
    # character digest from +type+, +local_type+ and +name+.
    #
    # Some regions define additional airspace types. In LF (France) for
    # intance, the types RMZ (radio mandatory zone) and TMZ (transponder
    # mandatory zone) exist. Such airspaces are usually specified together
    # with a generic type such as +:regulated_airspace+:
    #
    #   airspace= AIXM.airspace(type: :regulated_airspace, local_type: "RMZ")
    #
    # @see https://gitlab.com/openflightmaps/ofmx/wikis/Airspace#ase-airspace
    class Airspace < Feature
      include AIXM::Concerns::Association

      public_class_method :new

      TYPES = {
        NAS: :national_airspace_system,
        FIR: :flight_information_region,
        'FIR-P': :part_of_flight_information_region,
        UIR: :upper_flight_information_region,
        'UIR-P': :part_of_upper_flight_information_region,
        CTA: :control_area,
        'CTA-P': :part_of_control_area,
        OCA: :oceanic_control_area,
        'OCA-P': :part_of_oceanic_control_area,
        UTA: :upper_control_area,
        'UTA-P': :part_of_upper_control_area,
        TMA: :terminal_control_area,
        'TMA-P': :part_of_terminal_control_area,
        CTR: :control_zone,
        'CTR-P': :part_of_control_zone,
        CLASS: :airspace_with_class,
        OTA: :oceanic_transition_area,
        SECTOR: :control_sector,
        'SECTOR-C': :temporarily_consolidated_sector,
        TSA: :temporary_segregated_area,
        TRA: :temporary_reserved_area,
        CBA: :cross_border_area,
        RCA: :reduced_coordination_airspace_procedure,
        RAS: :regulated_airspace,
        AWY: :airway,
        P: :prohibited_area,
        R: :restricted_area,
        'R-AMC': :amc_manageable_restricted_area,
        D: :danger_area,
        'D-AMC': :amc_manageable_danger_area,
        'D-OTHER': :dangerous_activities_area,
        ADIZ: :air_defense_identification_zone,
        A: :alert_area,
        W: :warning_area,
        PROTECT: :protected_from_specific_air_traffic,
        AMA: :minimum_altitude_area,
        ASR: :altimeter_setting_region,
        'NO-FIR': :airspace_outside_any_flight_information_region,
        POLITICAL: :political_area,
        PART: :part_of_airspace
      }.freeze

      # @!method geometry
      #   @return [AIXM::Component::Geometry] horizontal geometry shape
      #
      # @!method geometry=(geometry)
      #   @param geometry [AIXM::Component::Geometry]
      has_one :geometry

      # @!method layers
      #   @return [Array<AIXM::Compoment::Layer>] vertical layers
      #
      # @!method add_layer(layer)
      #   @param layer [AIXM::Compoment::Layer]
      has_many :layers

      # Published identifier (e.g. "LFP81").
      #
      # @note When assigning +nil+, a 4 byte hex derived from {#type}, {#name}
      #   and {#local_type} is written instead.
      #
      # @overload id
      #   @return [String]
      # @overload id=(value)
      #   @param value [String]
      attr_reader :id

      # Type of airspace (see {TYPES})
      #
      # @overload type
      #   @return [Symbol] any of {TYPES}
      # @overload type=(value)
      #   @param value [Symbol] any of {TYPES}
      attr_reader :type

      # Local type.
      #
      # Some regions define additional local types such as "RMZ" or "TMZ". They
      # are often further specifying type +:regulated_airspace+.
      #
      # @overload local_type
      #   @return [String, nil]
      # @overload local_type=(value)
      #   @param value [String, nil]
      attr_reader :local_type

      # Full name (e.g. "LF P 81 CHERBOURG")
      #
      # @overload name
      #   @return [String, nil]
      # @overload name=(value)
      #   @param value [String, nil]
      attr_reader :name

      # Alternative name (e.g. "LF P 81")
      #
      # @overload alternative_name
      #   @return [String, nil]
      # @overload alternative_name=(value)
      #   @param value [String, nil]
      attr_reader :alternative_name

      # See the {cheat sheet}[AIXM::Feature::Airspace] for examples on how to
      # create instances of this class.
      def initialize(source: nil, region: nil, id: nil, type:, local_type: nil, name: nil)
        super(source: source, region: region)
        self.type, self.local_type, self.name = type, local_type, name
        self.id = id
        self.geometry = AIXM.geometry
      end

      # @return [String]
      def inspect
        %Q(#<#{self.class} type=#{type.inspect} name=#{name.inspect}>)
      end

      # The +id+ is mandatory, however, you may assign +nil+ which will generate
      # an 8 character digest from +type+, +local_type+ and +name+.
      def id=(value)
        fail(ArgumentError, "invalid id") unless value.nil? || value.is_a?(String)
        @id = value&.uptrans || [type, local_type, name].to_digest.upcase
      end

      def type=(value)
        @type = TYPES.lookup(value&.to_s&.to_sym, nil) || fail(ArgumentError, "invalid type")
      end

      def local_type=(value)
        fail(ArgumentError, "invalid short name") unless value.nil? || value.is_a?(String)
        @local_type = value&.uptrans
      end

      def name=(value)
        fail(ArgumentError, "invalid name") unless value.nil? || value.is_a?(String)
        @name = value&.uptrans
      end

      def alternative_name=(value)
        fail(ArgumentError, "invalid alternative name") unless value.nil? || value.is_a?(String)
        @alternative_name = value&.uptrans
      end

      # @!visibility private
      def add_uid_to(builder, as: :AseUid)
        builder.send(as, ({ region: (region if AIXM.ofmx?) }.compact)) do |tag|
          tag.codeType(TYPES.key(type))
          tag.codeId(id)
          tag.txtLocalType(local_type) if AIXM.ofmx? && local_type && local_type != name
        end
      end

      # @!visibility private
      def add_wrapped_uid_to(builder, as: :AseUid, with:)
        builder.send(with) do |tag|
          add_uid_to(tag, as: as)
        end
      end

      # @!visibility private
      def add_to(builder)
        fail(LayerError.new("no layers defined", self)) unless layers.any?
        builder.comment "Airspace: [#{TYPES.key(type)}] #{name || :UNNAMED}".dress
        builder.text "\n"
        builder.Ase({ source: (source if AIXM.ofmx?) }.compact) do |ase|
          ase.comment(indented_comment) if comment
          add_uid_to(ase)
          ase.txtLocalType(local_type) if AIXM.aixm? && local_type && local_type != name
          ase.txtName(name) if name
          ase.txtNameAlt(alternative_name) if AIXM.ofmx? && alternative_name
          layers.first.add_to(ase) unless layered?
        end
        builder.Abd do |abd|
          add_wrapped_uid_to(abd, with: :AbdUid)
          geometry.add_to(abd)
        end
        if layered?
          layers.each.with_index do |layer, index|
            layer_airspace = AIXM.airspace(region: region, type: 'CLASS', name: "#{name} LAYER #{index + 1}")
            builder.Ase do |ase|
              layer_airspace.add_uid_to(ase)
              ase.txtName(layer_airspace.name)
              layers[index].add_to(ase)
            end
            builder.Adg do |adg|
              layer_airspace.add_wrapped_uid_to(adg, with: :AdgUid)
              add_uid_to(adg, as: :AseUidSameExtent)
            end
            layer.services.each do |service|
              builder.Sae do |sae|
                sae.SaeUid do |sae_uid|
                  service.add_uid_to(sae_uid)
                  layer_airspace.add_uid_to(sae_uid)
                end
              end
            end
          end
        else
          layers.first.services.each do |service|
            builder.Sae do |sae|
              sae.SaeUid do |sae_uid|
                service.add_uid_to(sae_uid)
                add_uid_to(sae_uid)
              end
            end
          end
        end
      end

      private

      def layered?
        layers.count > 1
      end
    end
  end
end