Sunday 25 November 2018

aviation_gis_tools: bearings, magnetic variation

In this post we will discuss another indispensable 'item' when we deal with aviation content:  bearings (true and magnetic) and magnetic variations.

Many times in aeronautical publications you will see:
  • position of points are defined as magnetic bearing from specified point (e. g. aerodrome reference point), and magnetic variation for this point is known, we are interesting in  geographic position of obstacle  not magnetic
  • boundary of airspace (a good example that comes to my mind are airspaces on minimum radar vectoring chart) are defined as arcs and 'strait' lines by points, which are defined by radials and distances against point with specified position and magnetic variation; by definition radial are expressed in magnetic bearing - and again we would like to know geographic position of vertices that create the shape of airspace not magnetic.
As you can see - if we do not have a tool that can check if input is correct bearing, magnetic variation and calculate true bearing little can be done.

Because bearing and magnetic variation are types of angles (as well as latitude and longitude) and might be given in various formats let's create basic class Angle.
Angle class will cover common tasks as validating input, range and so on.
Class definition will start with constructor:


class Angle:
    def __init__(self, src_value):
        self.src_value = src_value
        self._is_valid = None
        self._dd_value = None
        self._err_msg = ''

where src_value is value of angle (bearing, magnetic variation) form source (NOTAM, AIP etc.)

Attribute  _is_valid will take value True if  input src_value will be valid angle and False otherwise.
Because for further calculations we need value in decimal degrees, attribute _dd_value will keep DD
for passed value from source - only if value is valid for specified angle type, such as bearing, or magnetic variation.
Angle class will be basic class for other classes (to deal with bearings and magnetic variation) so validation of angle will be covered in appropriate subclasses.

You might ask now: Where are latitude and longitude types?

Angle class will contain methods to validate various formats of angle (DD, DMS etc.) that are common for all types of angles (bearing, magnetic variation, coordinates). Specific formats for specified types of angle will be implemented in appropriate subclasses.

I assume that src_value might be passed by 'copy and paste' methos, hence first of all I need to normalize input, e. g. trim leading and trailing blanks characters and etc. I have crated method
normalize_src_input do deal with that:


@staticmethod
def normalize_src_input(src_input):
    """ Normalizes source (input) angle for further processing
    :param src_input: str, input angle string to normalize
    :return: norm_angle: str, normalized angle string

    """
    norm_input = str(src_input)
    norm_input = norm_input.replace(',', '.')
    norm_input = norm_input.upper()
    return norm_input

Now we write function that will check if input data is correct. Let's start with very basic formats: so far the only angle format that is valid is decimal degrees. Function to validate if input is in decimal degrees:


@staticmethod
def check_angle_dd(angle):
    """ Checks if angle is in DD format.
    :param angle: float, str: angle to check
    :return: float, vale of angle if angle is integer of float, const NOT_VALID otherwise
    """
    try:
        a = float(angle)
        return a
    except ValueError:
        return None


Next step is the function that will check if the angle in DD format is within appropriate range, witch depends on type of angle (e.g. absolute bearing <0, 360>, latitude <-90, 90):

@staticmethod
def check_angle_range(angle, min_value, max_value):
    if min_value <= angle <= max_value:
        return True, angle
    else:
        return False, None


Magnetic variation
For the time being this we will cover following formats of magnetic variation:
  1. DD format (e. g. -3.55, 0.7)
  2. DD format with magnetic variation letter prefix or suffix (e. g. W3.55, E0.7)

class MagVar(Angle):
    def __init__(self, mag_var_src):
        Angle.__init__(self, mag_var_src)
        self.parse_mag_var()

First case (DD) is covered by Angle class, so we need only write method that checks if magnetic variation is in second format:


def check_magvar_vletter_dd(self, mag_var):
    """ Check if magnetic variation is in decimal degrees with variation letter suffix or prefix format.
    e. g.: E3.55, 0.77W
    :return: float - magnetic variation in decimal degrees, or bool - False if input outside the range
    """
    if REGEX_MAG_VAR_VLDD.match(mag_var):
        h = mag_var[0]
        mv = self.check_angle_dd(mag_var[1:])
        if mv != NOT_VALID:
            if h == 'W':
                mv = -mv
            return mv
        else:
            return None
    elif REGEX_MAG_VAR_DDVL.match(mag_var):
        h = mag_var[-1]
        mv = self.check_angle_dd(mag_var[:-1])
        if mv != NOT_VALID:
            if h == 'W':
                mv = -mv
            return mv
        else:
            return None
    else:
        return None

And I used following regular expressions:


REGEX_MAG_VAR_VLDD = re.compile(r'^[WE]\d+\.\d+$|^[WE]\d+$')
REGEX_MAG_VAR_DDVL = re.compile(r'^\d+\.\d+[WE]$|^\d+[WE]$')

And finally, function that parse input (src_value) and ties convert it into DD format:


def parse_mag_var(self):
    """ Parse source value to convert it into decimal degrees value"""
    if self.src_value == '':  # If no value given - by default magnetic variation is 0.0
        self.dd_value = 0.0
        self.is_valid = True
        return
    else:
        norm_src = self.normalize_src_input(self.src_value)
        mv_dd = self.check_angle_dd(norm_src)  # Check if magnetic variation is in DD format
        if mv_dd is None:
            mv_dd = self.check_magvar_vletter_dd(norm_src)  # Check if it is in HDD or DDH format
            if mv_dd is None:
                self.is_valid = False
                self.err_msg = 'Magnetic variation error!\n'

        if mv_dd is not None:  # Managed to get DD format of magnetic variation - check if it is within range
            self.is_valid, self.dd_value = self.check_angle_range(mv_dd, -120, 120)

        if self.is_valid is False:
            self.err_msg = 'Magnetic variation error!\n'

Bearing class
Constructor:


class Bearing(Angle):
    def __init__(self, brng_src):
        Angle.__init__(self, brng_src)
        self.dd_tbrng = None
        self.parse_brng()

Check if src_value of bearing is correct:


def parse_brng(self):
    """ Parse source value to convert it into decimal degrees value"""
    if self.src_value == '':  # No value
        self.is_valid = False
        self.err_msg = 'Enter bearing!\n'
    else:
        norm_src = self.normalize_src_input(self.src_value)
        brng = self.check_angle_dd(norm_src)  # Check if bearing is given in decimal degrees format
        if brng is None:
            self.is_valid = False
            self.err_msg = 'Bearing error!\n'

        if brng is not None:  # Managed to get DD format of bearing - check if it is within range
            self.is_valid, self.dd_value = self.check_angle_range(brng, 0, 360)

        if self.is_valid is False:
            self.err_msg = 'Bearing error!\n'

Calculate true bearing is done by method:


def calc_tbrng(self, dd_mag_var):
    """ Calculates true bearing.
    :param: dd_mag_var: float, magnetic variation value
    """
    if dd_mag_var == 0:
        self.dd_tbrng = self.dd_value
    else:
        self.dd_tbrng = self.dd_value + dd_mag_var
        if self.dd_tbrng > 360:
            self.dd_tbrng -= 360
        elif self.dd_tbrng < 360:
            self.dd_tbrng += 360



No comments:

Post a Comment