# -*- coding: utf-8 -*-
"""
Created on Thu Sep 1 11:43:56 2022
@author: jpeacock
"""
# =============================================================================
# Imports
# =============================================================================
import dateutil
import logging
# =============================================================================
[docs]class GPSError(Exception):
pass
[docs]class GPS(object):
"""
class to parse GPS stamp from the NIMS
Depending on the type of Stamp different attributes will be filled.
GPRMC has full date and time information and declination
GPGGA has elevation data
.. note:: GPGGA date is set to 1980-01-01 so that the time can be estimated.
Should use GPRMC for accurate date/time information.
"""
def __init__(self, gps_string, index=0):
self.logger = logging.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.gps_string = gps_string
self.index = index
self._type = None
self._time = None
self._date = "010180"
self._latitude = None
self._latitude_hemisphere = None
self._longitude = None
self._longitude_hemisphere = None
self._declination = None
self._declination_hemisphere = None
self._elevation = None
self.valid = False
self.elevation_units = "meters"
self.type_dict = {
"gprmc": {
0: "type",
1: "time",
2: "fix",
3: "latitude",
4: "latitude_hemisphere",
5: "longitude",
6: "longitude_hemisphere",
7: "skip",
8: "skip",
9: "date",
10: "declination",
11: "declination_hemisphere",
"length": [12],
"type": 0,
"time": 1,
"fix": 2,
"latitude": 3,
"latitude_hemisphere": 4,
"longitude": 5,
"longitude_hemisphere": 6,
"date": 9,
"declination": 10,
},
"gpgga": {
0: "type",
1: "time",
2: "latitude",
3: "latitude_hemisphere",
4: "longitude",
5: "longitude_hemisphere",
6: "var_01",
7: "var_02",
8: "var_03",
9: "elevation",
10: "elevation_units",
11: "elevation_error",
12: "elevation_error_units",
13: "null_01",
14: "null_02",
"length": [14, 15],
"type": 0,
"time": 1,
"latitude": 2,
"latitude_hemisphere": 3,
"longitude": 4,
"longitude_hemisphere": 5,
"elevation": 9,
"elevation_units": 10,
"elevation_error": 11,
"elevation_error_units": 12,
},
}
self.parse_gps_string(self.gps_string)
def __str__(self):
"""string representation"""
msg = [
f"type = {self.gps_type}",
f"index = {self.index}",
f"fix = {self.fix}",
f"time_stamp = {self.time_stamp}",
f"latitude = {self.latitude}",
f"longitude = {self.longitude}",
f"elevation = {self.elevation}",
f"declination = {self.declination}",
]
return "\n".join(msg)
def __repr__(self):
return self.__str__()
[docs] def validate_gps_string(self, gps_string):
"""
make sure the string is valid, remove any binary numbers and find
the end of the string as '*'
:param string gps_string: raw GPS string to be validated
:returns: validated string or None if there is something wrong
"""
if isinstance(gps_string, bytes):
for replace_str in [b"\xd9", b"\xc7", b"\xcc"]:
gps_string = gps_string.replace(replace_str, b"")
### sometimes the end is set with a zero for some reason
gps_string = gps_string.replace(b"\x00", b"*")
if gps_string.find(b"*") < 0:
logging.debug(f"GPSError: No end to stamp {gps_string}")
return None
else:
try:
gps_string = gps_string[0 : gps_string.find(b"*")].decode()
return gps_string
except UnicodeDecodeError:
logging.debug(
f"GPSError: stamp not correct format, {gps_string}"
)
return None
elif isinstance(gps_string, str):
if "*" not in gps_string:
logging.debug(f"GPSError: No end to stamp {gps_string}")
return None
return gps_string[0 : gps_string.find("*")]
else:
raise TypeError(
f"input must be a string or bytes object, not {type(gps_string)}"
)
def _split_gps_string(self, gps_string, delimiter=","):
"""Split a GPS string by ',' and validate it"""
gps_string = self.validate_gps_string(gps_string)
if gps_string is None:
self.valid = False
return []
return gps_string.strip().split(",")
[docs] def parse_gps_string(self, gps_string):
"""
Parse a raw gps string from the NIMS and set appropriate attributes.
GPS string will first be validated, then parsed.
:param string gps_string: raw GPS string to be parsed
"""
gps_list = self._split_gps_string(gps_string)
if gps_list == []:
self.logger.debug(f"GPS string is invalid, {gps_string}")
return
if len(gps_list) > 1:
if len(gps_list[1]) > 6:
self.logger.debug(
"GPS time and lat missing a comma adding one, check time"
)
gps_list = (
gps_list[0:1]
+ [gps_list[1][0:6], gps_list[1][6:]]
+ gps_list[2:]
)
### validate the gps list to make sure it is usable
gps_list, error_list = self.validate_gps_list(gps_list)
if len(error_list) > 0:
for error in error_list:
logging.debug("GPSError: " + error)
if gps_list is None:
return
attr_dict = self.type_dict[gps_list[0].lower()]
for index, value in enumerate(gps_list):
setattr(self, "_" + attr_dict[index], value)
if None not in gps_list:
self.valid = True
self.gps_string = gps_string
[docs] def validate_gps_list(self, gps_list):
"""
check to make sure the gps stamp is the correct format, checks each element
for the proper format
:param gps_list: a parsed gps string from a NIMS
:type gps_list: list
:raises: :class:`mth5.io.nims.GPSError` if anything is wrong.
"""
error_list = []
try:
gps_list = self._validate_gps_type(gps_list)
except GPSError as error:
error_list.append(error.args[0])
return None, error_list
### get the string type
g_type = gps_list[0].lower()
### first check the length, if it is not the proper length then
### return, cause you never know if everything else is correct
try:
self._validate_list_length(gps_list)
except GPSError as error:
error_list.append(error.args[0])
return None, error_list
try:
gps_list[self.type_dict[g_type]["time"]] = self._validate_time(
gps_list[self.type_dict[g_type]["time"]]
)
except GPSError as error:
error_list.append(error.args[0])
gps_list[self.type_dict[g_type]["time"]] = None
try:
gps_list[
self.type_dict[g_type]["latitude"]
] = self._validate_latitude(
gps_list[self.type_dict[g_type]["latitude"]],
gps_list[self.type_dict[g_type]["latitude_hemisphere"]],
)
except GPSError as error:
error_list.append(error.args[0])
gps_list[self.type_dict[g_type]["latitude"]] = None
try:
gps_list[
self.type_dict[g_type]["longitude"]
] = self._validate_longitude(
gps_list[self.type_dict[g_type]["longitude"]],
gps_list[self.type_dict[g_type]["longitude_hemisphere"]],
)
except GPSError as error:
error_list.append(error.args[0])
gps_list[self.type_dict[g_type]["longitude"]] = None
if g_type == "gprmc":
try:
gps_list[
self.type_dict["gprmc"]["date"]
] = self._validate_date(
gps_list[self.type_dict["gprmc"]["date"]]
)
except GPSError as error:
error_list.append(error.args[0])
gps_list[self.type_dict[g_type]["date"]] = None
elif g_type == "gpgga":
try:
gps_list[
self.type_dict["gpgga"]["elevation"]
] = self._validate_elevation(
gps_list[self.type_dict["gpgga"]["elevation"]]
)
except GPSError as error:
error_list.append(error.args[0])
gps_list[self.type_dict["gpgga"]["elevation"]] = None
return gps_list, error_list
def _validate_gps_type(self, gps_list):
"""Validate gps type should be gpgga or gprmc"""
gps_type = gps_list[0].lower()
if "gpg" in gps_type:
if len(gps_type) > 5:
gps_list = ["GPGGA", gps_type[-6:]] + gps_list[1:]
elif len(gps_type) < 5:
gps_list[0] = "GPGGA"
elif "gpr" in gps_type:
if len(gps_type) > 5:
gps_list = ["GPRMC", gps_type[-6:]] + gps_list[1:]
elif len(gps_type) < 5:
gps_list[0] = "GPRMC"
gps_type = gps_list[0].lower()
if gps_type not in ["gpgga", "gprmc"]:
raise GPSError(
"GPS String type not correct. "
f"Expect GPGGA or GPRMC, got {gps_type.upper()}"
)
return gps_list
def _validate_list_length(self, gps_list):
"""validate gps list length based on type of string"""
gps_list_type = gps_list[0].lower()
expected_len = self.type_dict[gps_list_type]["length"]
if len(gps_list) not in expected_len:
raise GPSError(
f"GPS string not correct length for {gps_list_type.upper()}. "
f"Expected {expected_len}, got {len(gps_list)} "
f"{','.join(gps_list)}"
)
def _validate_time(self, time_str):
"""validate time string, should be 6 characters long and an int"""
if len(time_str) != 6:
raise GPSError(
f"Length of time string {time_str} not correct. "
f"Expected 6 got {len(time_str)}. string = {time_str}"
)
try:
int(time_str)
except ValueError:
raise GPSError(f"Could not convert time string {time_str}")
return time_str
def _validate_date(self, date_str):
"""validate date string, should be 6 characters long and an int"""
if len(date_str) != 6:
raise GPSError(
f"Length of date string not correct {date_str}. "
f"Expected 6 got {len(date_str)}. string = {date_str}"
)
try:
int(date_str)
except ValueError:
raise GPSError(f"Could not convert date string {date_str}")
return date_str
def _validate_latitude(self, latitude_str, hemisphere_str):
"""validate latitude, should have hemisphere string with it"""
if len(latitude_str) < 8:
raise GPSError(
f"Latitude string should be larger than 7 characters. "
f"Got {len(latitude_str)}. string = {latitude_str}"
)
if len(hemisphere_str) != 1:
raise GPSError(
"Latitude hemisphere should be 1 character. "
f"Got {len(hemisphere_str)}. string = {hemisphere_str}"
)
if hemisphere_str.lower() not in ["n", "s"]:
raise GPSError(
f"Latitude hemisphere {hemisphere_str.upper()} not understood"
)
try:
float(latitude_str)
except ValueError:
raise GPSError(f"Could not convert latitude string {latitude_str}")
return latitude_str
def _validate_longitude(self, longitude_str, hemisphere_str):
"""validate longitude, should have hemisphere string with it"""
if len(longitude_str) < 8:
raise GPSError(
"Longitude string should be larger than 7 characters. "
f"Got {len(longitude_str)}. string = {longitude_str}"
)
if len(hemisphere_str) != 1:
raise GPSError(
"Longitude hemisphere should be 1 character. "
f"Got {len(hemisphere_str)}. string = {hemisphere_str}"
)
if hemisphere_str.lower() not in ["e", "w"]:
raise GPSError(
f"Longitude hemisphere {hemisphere_str.upper()} not understood"
)
try:
float(longitude_str)
except ValueError:
raise GPSError(
f"Could not convert longitude string {longitude_str}"
)
return longitude_str
def _validate_elevation(self, elevation_str):
"""validate elevation, check for converstion to float"""
elevation_str = elevation_str.lower().replace("m", "")
if elevation_str == "":
elevation_str = "0"
try:
elevation_str = f"{float(elevation_str)}"
except ValueError:
raise GPSError(f"Elevation could not be converted {elevation_str}")
return elevation_str
@property
def latitude(self):
"""
Latitude in decimal degrees, WGS84
"""
if (
self._latitude is not None
and self._latitude_hemisphere is not None
):
index = len(self._latitude) - 7
lat = (
float(self._latitude[0:index])
+ float(self._latitude[index:]) / 60
)
if "s" in self._latitude_hemisphere.lower():
lat *= -1
return lat
return 0.0
@property
def longitude(self):
"""
Latitude in decimal degrees, WGS84
"""
if (
self._longitude is not None
and self._longitude_hemisphere is not None
):
index = len(self._longitude) - 7
lon = (
float(self._longitude[0:index])
+ float(self._longitude[index:]) / 60
)
if "w" in self._longitude_hemisphere.lower():
lon *= -1
return lon
return 0.0
@property
def elevation(self):
"""
elevation in meters
"""
if self._elevation is not None:
try:
return float(self._elevation)
except ValueError:
self.logger.error(
"GPSError: Could not get elevation GPS string"
+ f"not complete {self.gps_string}"
)
return 0.0
@property
def time_stamp(self):
"""
return a datetime object of the time stamp
"""
if self._time is None:
return None
if self._date is None:
self._date = "010180"
try:
return dateutil.parser.parse(
"{0} {1}".format(self._date, self._time), dayfirst=True
)
except ValueError:
self.logger.error(f"GPSError: bad date string {self.gps_string}")
return None
@property
def declination(self):
"""
geomagnetic declination in degrees from north
"""
if self._declination is None or self._declination_hemisphere is None:
return None
dec = float(self._declination)
if "w" in self._declination_hemisphere.lower():
dec *= -1
return dec
@property
def gps_type(self):
"""GPRMC or GPGGA"""
return self._type
@property
def fix(self):
"""
GPS fixed
"""
if hasattr(self, "_fix"):
return self._fix
return None