Tuesday 4 December 2018

Coordinates (5): coordinate_tool module

So far we know how to deal with coordinates: how to check if the input is valid - and if it is - how to convert input into decimal degrees format. We have wrote a few functions, but use them might be not as convenient as we expect. Because latitude and longitude input will be very often use while dealing with aviation content, and some data such as: original input from source, information if input is valid coordinate, DD equivalent if input is valid or information about what is wrong with input, etc. will be useful in further processing a good idea is to write class which will deal with all the stuff I mention above.


CoordinatePair class is class to check if input coordinates are valid, and keeps relevant information such as: if input is valid, error message and decimal degrees format. The purpose of CoordinatePair class is to easy access to validation coordinates in further scripts and plugins.
Basically, checking if coordinate is valid is split into two parts: check if coordinate is in compacted format and is in separated (or delimited) format.

Let's create new python file: aviation_coordinate_tools.py and start populate it with the code.

First, definition of constant:
Types of coordinates:
    C_LAT = 'C_LAT'
    C_LON = 'C_LON'
    

    Hemisphere indicators:
      H_LAT = ['N', 'S']
      H_LON = ['E', 'W']
      H_NEGATIVE = ['-', 'S', 'W']
      H_ALL = ['-', '+', 'N', 'S', 'E', 'W']
      

      Types of separators:
        S_SPACE = ' '
        S_HYPHEN = '-'
        S_DEG_WORD = 'DEG'
        S_DEG_LETTER = 'D'
        S_MIN_WORD = 'MIN'
        S_MIN_LETTER = 'M'
        S_SEC_WORD = 'SEC'
        S_ALL = [S_SPACE, S_HYPHEN, S_DEG_WORD, S_DEG_LETTER, S_MIN_WORD, S_MIN_LETTER, S_SEC_WORD]
        

        Types of  compacted formats:


        F_HDMS_COMP = 'F_HDMS_COMP'  # Hemisphere prefix DMS compacted
        F_DMSH_COMP = 'F_DMSH_COMP'  # Hemisphere suffix DMS compacted
        F_HDM_COMP = 'F_HDM_COMP'  # Hemisphere prefix DMS compacted
        F_DMH_COMP = 'F_DMH_COMP'  # Hemisphere suffix DMS compacted
        

        Regular expressions for compacted formats:

        coord_regex = {F_HDMS_COMP: re.compile(r'''(?P<hem>^[NSEW])
                                                   (?P<deg>\d{2,3})  # Degrees
                                                   (?P<min>\d{2})  # Minutes
                                                   (?P<sec>\d{2}(\.\d+)?$)  # Seconds 
                                                ''', re.VERBOSE),
                       F_DMSH_COMP: re.compile(r'''(?P<deg>^\d{2,3})  # Degrees
                                                   (?P<min>\d{2})  # Minutes
                                                   (?P<sec>\d{2}(\.\d+)?)  # Seconds
                                                   (?P<hem>[NSEW]$)   
                                                ''', re.VERBOSE),
                       F_HDM_COMP: re.compile(r'''(?P<hem>^[NSEW])
                                                  (?P<deg>\d{2,3})  # Degrees
                                                  (?P<min>\d{2}(\.\d+)?$)  # Minutes
                                                ''', re.VERBOSE),
                       F_DMH_COMP: re.compile(r'''(?P<deg>^\d{2,3})  # Degrees
                                                                  (?P<min>\d{2}(\.\d+)?)  # Minutes
                                                                  
                                                                  (?P<hem>[NSEW]$)   
                                                               ''', re.VERBOSE)
                       }
        

        CoordinatesPair class constructor:

        class CoordinatesPair:
            def __init__(self, src_lat, src_lon):
                self.src_lat = src_lat
                self.src_lon = src_lon
                self._is_valid = None
                self._err_msg = ''
                self._dd_lat = None
                self._dd_lon = None
                self.parse_src_coordinates()
        

        Method that 'normalizes' input (source) coordinates:


            @staticmethod
            def normalize_src_input(src_input):
                norm_input = str(src_input)
                norm_input = norm_input.strip()  # Trim leading and trailing space
                norm_input = norm_input.upper()  # Make all letters capitals
                norm_input = norm_input.replace(',', '.')  # Make sure that decimal separator is dot not comma
                return norm_input
        

        Method that checks if input coordinate is in compacted format:

            @staticmethod
            def parse_regex(regex_patterns, dms, c_type):
                dd = None
                for pattern in regex_patterns:  # Check if input matches any pattern
                    if regex_patterns.get(pattern).match(dms):
                        if pattern in [F_DMSH_COMP, F_HDMS_COMP]:
                            # If input matches to pattern get hemisphere, degrees, minutes and seconds values
                            groups = regex_patterns.get(pattern).search(dms)
                            h = groups.group('hem')
                            d = float(groups.group('deg'))
                            m = float(groups.group('min'))
                            s = float(groups.group('sec'))
        
                            if (h in H_LAT and c_type == C_LAT) or (h in H_LON and c_type == C_LON):
        
                                if h in ['N', 'S']:
                                    if d > 90:  # Latitude is in range <-90, 90>
                                        dd = None
                                    elif d == 90 and (m > 0 or s > 0):
                                        dd = None
                                    else:
                                        if m >= 60 or s >= 60:
                                            dd = None
                                        else:
                                            dd = d + m / 60 + s / 3600
                                            if h == 'S':
                                                dd = -dd
        
                                elif h in ['E', 'W']:
                                    if d > 180:  # Longitude is in range <-180, 180>
                                        dd = None
                                    elif d == 180 and (m > 0 or s > 0):
                                        dd = None
                                    else:
                                        if m >= 60 or s >= 60:
                                            dd = None
                                        else:
                                            dd = d + m / 60 + s / 3600
                                            if h == 'W':
                                                dd = -dd
        
                return dd
        

        Method that checks if coordinate is in separated format:


        @staticmethod
            def parse_coordinate(coord_norm, c_type):
        
                dd = None
        
                # First, check if input is in DD format
                try:
                    dd = float(coord_norm)
                except ValueError:
                    # Assume that coordinate is in DMS, DMSH, DM, DMH, HDD, DDH
                    # Check first and last character
                    h = coord_norm[0]
                    if h in H_ALL:  # DMS, DM signed or HDMS, HDM, HDD,
                        coord_norm = coord_norm[1:]
                    else:  # Check last character
                        h = coord_norm[-1]
                        if h in H_ALL:
                            coord_norm = coord_norm[:-1]
                        else:
                            h = coord_norm[0]
                            if h.isdigit():
                                if c_type == C_LAT:
                                    h = 'N'
                                elif c_type == C_LON:
                                    h = 'E'
        
                    # Check if hemisphere letter matches coordinate type (c_type)
                    if (h in H_LAT and c_type == C_LAT) or (h in H_LON and c_type == C_LON):
                        # Trim spaces again
                        coord_norm = coord_norm.strip()
                        # Replace separators (delimiters) with blank (space)
                        for sep in S_ALL:
                            coord_norm = re.sub(sep, ' ', coord_norm)
                        # Replace multiple spaces into single spaces
                        coord_norm = re.sub('\s+', ' ', coord_norm)
                        c_parts = coord_norm.split(' ')
                        if len(c_parts) == 3:  # Assume format DMS separated
        
                            try:
                                d = int(c_parts[0])
                                if d < 0:
                                    return None
                            except ValueError:
                                return None
        
                            try:
                                m = int(c_parts[1])
                                if m < 0 or m >= 60:
                                    return None
                            except ValueError:
                                return None
        
                            try:
                                s = float(c_parts[2])
                                if s < 0 or s >= 60:
                                    return None
                            except ValueError:
                                return None
        
                            try:
                                dd = float(d) + float(m) / 60 + s / 3600
                                if h in H_NEGATIVE:
                                    dd = - dd
                            except ValueError:
                                return None
        
                        elif len(c_parts) == 2:  # Assume format DM separated
                            try:
                                d = int(c_parts[0])
                                if d < 0:
                                    return None
                            except ValueError:
                                return None
        
                            try:
                                m = float(c_parts[1])
                                if m < 0 or m >= 60:
                                    return None
                            except ValueError:
                                return None
        
                            try:
                                dd = float(d) + m / 60
                                if h in H_NEGATIVE:
                                    dd = - dd
                            except ValueError:
                                return None
        
                        elif len(c_parts) == 1:  # Assume format DMS, DM compacted or DD
                            try:
                                dd = float(c_parts[0])
                                if h in H_NEGATIVE:
                                    dd = -dd
                            except ValueError:
                                return None
                    else:
                        return None
        
                # If we get dd - check is is withing range
                if dd is not None:
                    if c_type == C_LAT:
                        if -90 <= dd <= dd:
                            return dd
                        else:
                            return None
                    elif c_type == C_LON:
                        if -180 <= dd <= 180:
                            return dd
                        else:
                            return None
        

        Method that parses input coordinates:


        def parse_src_coordinates(self):
                if self.src_lat == '':  # Blank input
                    self._err_msg += 'Enter latitude value!\n'
                else:
                    norm_lat = self.normalize_src_input(self.src_lat)
                    self.dd_lat = self.parse_regex(coord_regex, norm_lat, C_LAT)
                    if self.dd_lat is None:
                        self.dd_lat = self.parse_coordinate(norm_lat, C_LAT)
                        if self.dd_lat is None:
                            self.err_msg += 'Latitude value wrong value!\n'
        
                if self.src_lon == '':  # Blank input
                    self.err_msg += 'Enter longitude value!\n'
                else:
                    norm_lon = self.normalize_src_input(self.src_lon)
                    self.dd_lon = self.parse_regex(coord_regex, norm_lon, C_LON)
                    if self.dd_lon is None:
                        self.dd_lon = self.parse_coordinate(norm_lon, C_LON)
                        if self.dd_lon is None:
                            self.err_msg += 'Longitude value wrong value!\n'
        
                if self.dd_lat is not None and self.dd_lon is not None:
                    self.is_valid = True
                else:
                    self.is_valid = False
        



        No comments:

        Post a Comment