lib/aixm/feature/navigational_aid/dme.rb

Summary

Maintainability
A
0 mins
Test Coverage
using AIXM::Refinements

module AIXM
  class Feature
    class NavigationalAid

      # Distance measuring equipment (DME) is a transponder-based radio navigation
      # technology which measures slant range distance by timing the propagation
      # delay of VHF or UHF signals. They operate in the frequency band between
      # 962 MHz and 1213 MHz.
      #
      # ===Cheat Sheet in Pseudo Code:
      #   dme = AIXM.dme(
      #     source: String or nil
      #     region: String or nil
      #     organisation: AIXM.organisation
      #     id: String
      #     name: String
      #     xy: AIXM.xy
      #     z: AIXM.z or nil
      #     channel: String   # either set channel directly
      #     ghost_f: AIXM.f   # or set channel via VOR ghost frequency
      #   )
      #   dme.timetable = AIXM.timetable or nil
      #   dme.remarks = String or nil
      #   dme.comment = Object or nil
      #
      # @see https://gitlab.com/openflightmaps/ofmx/wikis/Navigational-aid#dme-dme
      class DME < NavigationalAid
        public_class_method :new

        CHANNEL_RE = /\A([1-9]|[1-9]\d|1[0-1]\d|12[0-6])[XY]\z/.freeze

        GHOST_MAP = {
          108_00 => (17..59),
          112_30 => (70..126),
          133_30 => (60..69),
          134_40 => (1..16)
        }.freeze

        # @!method vor
        #   @return [AIXM::Feature::NavigationalAid::VOR, nil] associated VOR
        belongs_to :vor, readonly: true

        # Radio channel
        #
        # @overload channel
        #   @return [String]
        # @overload channel=(value)
        #   @param value [String]
        # @overload ghost_f
        #   @return [AIXM::F] ghost frequency matching the {#channel}
        # @overload ghost_f=(value)
        #   @param value [AIXM::F] ghost frequency matching the {#channel}
        attr_reader :channel

        # See the {cheat sheet}[AIXM::Feature::NavigationalAid::DME] for examples
        # on how to create instances of this class.
        def initialize(channel: nil, ghost_f: nil, **arguments)
          super(**arguments)
          case
            when channel then self.channel = channel
            when ghost_f then self.ghost_f = ghost_f
            else fail(ArgumentError, "either channel or ghost_f must be set")
          end
        end

        def channel=(value)
          fail(ArgumentError, "invalid channel") unless value.is_a?(String) && value.match?(CHANNEL_RE)
          @channel = value
        end

        def ghost_f=(value)
          fail(ArgumentError, "invalid ghost_f") unless value.is_a?(AIXM::F) && value.unit == :mhz
          integer, letter = (value.freq * 100).round, 'X'
          unless (integer % 10).zero?
            integer -= 5
            letter = 'Y'
          end
          base = GHOST_MAP.keys.reverse.bsearch { _1 <= integer }
          number = ((integer - base) / 10) + GHOST_MAP[base].min
          self.channel = "#{number}#{letter}"
        end

        def ghost_f
          if channel
            number, letter = channel.split(/(?=[XY])/)
            integer = GHOST_MAP.find { _2.include?(number.to_i) }.first
            integer += (number.to_i - GHOST_MAP[integer].min) * 10
            integer += 5 if letter == 'Y'
            AIXM.f(integer.to_f / 100, :mhz)
          end
        end

        # @!visibility private
        def add_uid_to(builder)
          builder.DmeUid({ region: (region if AIXM.ofmx?) }.compact) do |dme_uid|
            dme_uid.codeId(id)
            dme_uid.geoLat(xy.lat(AIXM.schema))
            dme_uid.geoLong(xy.long(AIXM.schema))
          end
        end

        # @!visibility private
        def add_to(builder)
          super
          builder.Dme({ source: (source if AIXM.ofmx?) }.compact) do |dme|
            dme.comment(indented_comment) if comment
            add_uid_to(dme)
            organisation.add_uid_to(dme)
            vor.add_uid_to(dme) if vor
            dme.txtName(name) if name
            dme.codeChannel(channel)
            unless vor
              dme.valGhostFreq(ghost_f.freq.trim)
              dme.uomGhostFreq('MHZ')
            end
            dme.codeDatum('WGE')
            if z
              dme.valElev(z.alt)
              dme.uomDistVer(z.unit.upcase)
            end
            timetable.add_to(dme, as: :Dtt) if timetable
            dme.txtRmk(remarks) if remarks
          end
        end
      end

    end
  end
end