lib/aixm/component/runway.rb
using AIXM::Refinements
module AIXM
class Component
# Runways are landing and takeoff strips for forward propelled aircraft.
#
# By convention, the runway name is usually the composition of the runway
# forth name (smaller number) and the runway back name (bigger number)
# joined with a forward slash e.g. "12/30" or "16R/34L".
#
# A runway has one or to directions accessible as +runway.forth+ (mandatory)
# and +runway.back+ (optional). Both have identical properties.
#
# ===Cheat Sheet in Pseudo Code:
# runway = AIXM.runway(
# name: String
# )
# runway.dimensions = AIXM.r or nil
# runway.surface = AIXM.surface
# runway.marking = String or nil
# runway.status = STATUSES or nil
# runway.remarks = String or nil
# runway.forth.name = AIXM.a # preset based on the runway name
# runway.forth.geographic_bearing = AIXM.a or nil
# runway.forth.xy = AIXM.xy # center point at beginning edge of runway
# runway.forth.z = AIXM.z or nil # center point at beginning edge of runway
# runway.forth.touch_down_zone_z = AIXM.z or nil
# runway.forth.displaced_threshold = AIXM.d or nil # sets displaced_threshold_xy as well
# runway.forth.displaced_threshold_xy = AIXM.xy or nil # sets displaced_threshold as well
# runway.forth.vasis = AIXM.vasis or nil (default: unspecified VASIS)
# runway.forth.add_lighting = AIXM.lighting
# runway.forth.add_approach_lighting = AIXM.approach_lighting
# runway.forth.vfr_pattern = VFR_PATTERNS or nil
# runway.forth.remarks = String or nil
#
# @example Bidirectional runway
# runway = AIXM.runway(name: '16L/34R')
# runway.name # => '16L/34R'
# runway.forth.name.to_s = '16L'
# runway.forth.geographic_bearing = 165
# runway.back.name.to_s = '34R'
# runway.back.geographic_bearing = 345
#
# @example Unidirectional runway:
# runway = AIXM.runway(name: '16L')
# runway.name # => '16L'
# runway.forth.name.to_s = '16L'
# runway.forth.geographic_bearing = 165
# runway.back = nil
#
# @see https://gitlab.com/openflightmaps/ofmx/wikis/Airport#rwy-runway
class Runway < Component
include AIXM::Concerns::Association
include AIXM::Concerns::Marking
include AIXM::Concerns::Remarks
STATUSES = {
CLSD: :closed,
WIP: :work_in_progress, # e.g. construction work
PARKED: :parked_aircraft, # parked or disabled aircraft on helipad
FAILAID: :visual_aids_failure, # failure or irregular operation of visual aids
SPOWER: :secondary_power, # secondary power supply in operation
OTHER: :other # specify in remarks
}.freeze
# @!method forth
# @return [AIXM::Component::Runway::Direction] main direction
#
# @!method forth=(forth)
# @param forth [AIXM::Component::Runway::Direction]
has_one :forth, accept: 'AIXM::Component::Runway::Direction'
# @!method back
# @return [AIXM::Component::Runway::Direction, nil] reverse direction
#
# @!method back=(back)
# @param back [AIXM::Component::Runway::Direction, nil]
has_one :back, accept: 'AIXM::Component::Runway::Direction', allow_nil: true
# @!method surface
# @return [AIXM::Component::Surface] surface of the runway
#
# @!method surface=(surface)
# @param surface [AIXM::Component::Surface]
has_one :surface, accept: 'AIXM::Component::Surface'
# @!method airport
# @return [AIXM::Feature::Airport] airport the runway belongs to
belongs_to :airport
# Full name of runway (e.g. "12/30" or "16L/34R")
#
# @overload name
# @return [String]
# @overload name=(value)
# @param value [String]
attr_reader :name
# Dimensions
#
# @overload dimensions
# @return [AIXM::R, nil]
# @overload dimensions=(value)
# @param value [AIXM::R, nil]
attr_reader :dimensions
# Status of the runway
#
# @overload status
# @return [Symbol, nil] any of {STATUSES} or +nil+ for normal operation
# @overload status=(value)
# @param value [Symbol, nil] any of {STATUSES} or +nil+ for normal
# operation
attr_reader :status
# See the {cheat sheet}[AIXM::Component::Runway] for examples on how to
# create instances of this class.
def initialize(name:)
self.name = name
@name.split("/").tap do |forth_name, back_name|
self.forth = Direction.new(name: AIXM.a(forth_name))
self.back = Direction.new(name: AIXM.a(back_name)) if back_name
fail(ArgumentError, "invalid name") unless !back || back.name.inverse_of?(@forth.name)
end
self.surface = AIXM.surface
end
# @return [String]
def inspect
%Q(#<#{self.class} airport=#{airport&.id.inspect} name=#{name.inspect}>)
end
def name=(value)
fail(ArgumentError, "invalid name") unless value.is_a? String
@name = value.uptrans
end
def dimensions=(value)
fail(ArgumentError, "invalid dimensions") unless value.nil? || value.is_a?(AIXM::R)
@dimensions = value
end
def status=(value)
@status = value.nil? ? nil : (STATUSES.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid status"))
end
# Center line of the runway
#
# The center line of unidirectional runwawys is calculated using the
# runway dimensions. If they are unknown, the calculation is not possible
# and this method returns +nil+.
#
# @return [AIXM::L, nil]
def center_line
if back || dimensions
AIXM.l.add_line_point(
xy: forth.xy,
z: forth.z
).add_line_point(
xy: (back&.xy || forth.xy.add_distance(dimensions.length, forth.geographic_bearing)),
z: back&.z
)
end
end
# @!visibility private
def add_uid_to(builder)
builder.RwyUid do |rwy_uid|
airport.add_uid_to(rwy_uid)
rwy_uid.txtDesig(name)
end
end
# @!visibility private
def add_to(builder)
builder.Rwy do |rwy|
add_uid_to(rwy)
if dimensions
rwy.valLen(dimensions.length.to_m.dim.trim)
rwy.valWid(dimensions.width.to_m.dim.trim)
rwy.uomDimRwy('M')
end
surface.add_to(rwy) if surface
rwy.codeSts(STATUSES.key(status)) if status
rwy.txtMarking(marking) if marking
rwy.txtRmk(remarks) if remarks
end
center_line.line_points.each do |line_point|
builder.Rcp do |rcp|
rcp.RcpUid do |rcp_uid|
add_uid_to(rcp)
rcp.geoLat(line_point.xy.lat(AIXM.schema))
rcp.geoLong(line_point.xy.long(AIXM.schema))
end
rcp.codeDatum('WGE')
if line_point.z
rcp.valElev(line_point.z.alt)
rcp.uomDistVer(line_point.z.unit.upcase)
end
end
end
%i(@forth @back).each do |direction|
if direction = instance_variable_get(direction)
direction.add_to(builder)
end
end
end
# Runway directions further describe each direction {#forth} and {#back}
# of a runway.
#
# @see https://gitlab.com/openflightmaps/ofmx/wikis/Airport#rdn-runway-direction
class Direction
include AIXM::Concerns::Association
include AIXM::Concerns::Memoize
include AIXM::Concerns::XMLBuilder
include AIXM::Concerns::Remarks
VFR_PATTERNS = {
L: :left,
R: :right,
E: :left_or_right
}.freeze
# @!method lightings
# @return [Array<AIXM::Component::Lighting>] installed lighting systems
#
# @!method add_lighting(lighting)
# @param lighting [AIXM::Component::Lighting]
# @return [self]
has_many :lightings, as: :lightable
# @!method approach_lightings
# @return [Array<AIXM::Component::ApproachLighting>] installed approach
# lighting systems
#
# @!method add_approach_lighting(approach_lighting)
# @param approach_lighting [AIXM::Component::ApproachLighting]
# @return [self]
has_many :approach_lightings, as: :approach_lightable
# @!method runway
# @return [AIXM::Component::Runway] runway the runway direction is
# further describing
belongs_to :runway, readonly: true
# Partial name of runway (e.g. "12" or "16L")
#
# @overload name
# @return [AIXM::A]
# @overload name=(value)
# @param value [AIXM::A]
attr_reader :name
# @return [AIXM::A, nil] (true) geographic bearing in degrees
attr_reader :geographic_bearing
# @return [AIXM::XY] center point at the beginning edge of the runway
attr_reader :xy
# @return [AIXM::Z, nil] elevation of the center point at the beginning
# edge of the runway in +qnh+
attr_reader :z
# @return [AIXM::Z, nil] elevation of the touch down zone in +qnh+
attr_reader :touch_down_zone_z
# @return [AIXM::D, nil] displaced threshold distance from edge of runway
attr_reader :displaced_threshold
# @return [AIXM::XY, nil] displaced threshold point
attr_reader :displaced_threshold_xy
# @return [AIXM::Component::VASIS, nil] visual approach slope indicator
# system
attr_reader :vasis
# @return [Symbol, nil] direction of the VFR flight pattern (see {VFR_PATTERNS})
attr_reader :vfr_pattern
# See the {cheat sheet}[AIXM::Component::Runway] for examples on how to
# create instances of this class.
def initialize(name:)
self.name = name
self.vasis = AIXM.vasis
end
# @return [String]
def inspect
%Q(#<#{self.class} airport=#{runway&.airport&.id.inspect} name=#{name.to_s(:runway).inspect}>)
end
def name=(value)
fail(ArgumentError, "invalid name") unless value.is_a? AIXM::A
@name = value
end
def geographic_bearing=(value)
return @geographic_bearing = nil if value.nil?
fail(ArgumentError, "invalid geographic bearing") unless value.is_a? AIXM::A
@geographic_bearing = 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 touch_down_zone_z=(value)
fail(ArgumentError, "invalid touch_down_zone_z") unless value.nil? || (value.is_a?(AIXM::Z) && value.qnh?)
@touch_down_zone_z = value
end
def displaced_threshold=(value)
if value
fail(ArgumentError, "invalid displaced threshold") unless value.is_a?(AIXM::D) && value.dim > 0
fail(RuntimeError, "xy required to calculate displaced threshold xy") unless xy
fail(RuntimeError, "geographic bearing required to calculate displaced threshold xy") unless geographic_bearing
@displaced_threshold = value
@displaced_threshold_xy = xy.add_distance(value, geographic_bearing)
else
@displaced_threshold = @displaced_threshold_xy = nil
end
end
def displaced_threshold_xy=(value)
if value
fail(ArgumentError, "invalid displaced threshold xy") unless value.is_a? AIXM::XY
fail(RuntimeError, "xy required to calculate displaced threshold") unless xy
@displaced_threshold_xy = value
@displaced_threshold = xy.distance(value)
else
@displaced_threshold = @displaced_threshold_xy = nil
end
end
def vasis=(value)
fail(ArgumentError, "invalid vasis") unless value.nil? || value.is_a?(AIXM::Component::VASIS)
@vasis = value
end
def vfr_pattern=(value)
@vfr_pattern = value.nil? ? nil : (VFR_PATTERNS.lookup(value.to_s.to_sym, nil) || fail(ArgumentError, "invalid VFR pattern"))
end
# @return [AIXM::A] magnetic bearing in degrees
def magnetic_bearing
if geographic_bearing && runway.airport.declination
geographic_bearing - runway.airport.declination
end
end
# @return [AIXM::XY] displaced threshold if any or edge of runway otherwise
def threshold_xy
displaced_threshold_xy || xy
end
# @!visibility private
def add_uid_to(builder)
builder.RdnUid do |rdn_uid|
runway.add_uid_to(rdn_uid)
rdn_uid.txtDesig(name.to_s(:runway))
end
end
# @!visibility private
def add_to(builder)
builder.Rdn do |rdn|
add_uid_to(rdn)
rdn.geoLat(threshold_xy.lat(AIXM.schema))
rdn.geoLong(threshold_xy.long(AIXM.schema))
rdn.valTrueBrg(geographic_bearing.to_s(:bearing)) if geographic_bearing
rdn.valMagBrg(magnetic_bearing.to_s(:bearing)) if magnetic_bearing
if touch_down_zone_z
rdn.valElevTdz(touch_down_zone_z.alt)
rdn.uomElevTdz(touch_down_zone_z.unit.upcase)
end
vasis.add_to(rdn) if vasis
rdn.codeVfrPattern(VFR_PATTERNS.key(vfr_pattern)) if vfr_pattern
rdn.txtRmk(remarks) if remarks
end
if displaced_threshold
builder.Rdd do |rdd|
rdd.RddUid do |rdd_uid|
add_uid_to(rdd_uid)
rdd_uid.codeType('DPLM')
rdd_uid.codeDayPeriod('A')
end
rdd.valDist(displaced_threshold.dim.trim)
rdd.uomDist(displaced_threshold.unit.upcase)
rdd.txtRmk(remarks) if remarks
end
end
lightings.each do |lighting|
lighting.add_to(builder, as: :Rls)
end
approach_lightings.each do |approach_lighting|
approach_lighting.add_to(builder, as: :Rda)
end
end
end
end
end
end