# -*- coding: utf-8 -*-
"""
Created on Wed Dec 23 21:30:36 2020
:copyright:
Jared Peacock (jpeacock@usgs.gov)
:license: MIT
"""
# =============================================================================
# Imports
# =============================================================================
import numpy as np
from xml.etree import cElementTree as et
from mt_metadata.base.helpers import write_lines, element_to_string
from mt_metadata.base import get_schema, Base
from .standards import SCHEMA_FN_PATHS
from mt_metadata.transfer_functions.tf import Declination
from mt_metadata.transfer_functions.io.emtfxml.metadata import helpers
# =============================================================================
attr_dict = get_schema("location", SCHEMA_FN_PATHS)
attr_dict.add_dict(Declination()._attr_dict, "declination")
# =============================================================================
[docs]class Location(Base):
__doc__ = write_lines(attr_dict)
def __init__(self, **kwargs):
self.declination = Declination()
super().__init__(attr_dict=attr_dict, **kwargs)
@property
def latitude(self):
return self._latitude
@latitude.setter
def latitude(self, lat):
self._latitude = self._assert_lat_value(lat)
@property
def longitude(self):
return self._longitude
@longitude.setter
def longitude(self, lon):
self._longitude = self._assert_lon_value(lon)
@property
def elevation(self):
return self._elevation
@elevation.setter
def elevation(self, elev):
self._elevation = self._assert_elevation_value(elev)
def _assert_lat_value(self, latitude):
"""
Make sure the latitude value is in decimal degrees, if not change it.
And that the latitude is within -90 < lat > 90.
:param latitude: latitude in decimal degrees or other format
:type latitude: float or string
"""
if latitude in [None, "None", "none", "unknown"]:
self.logger.debug("Latitude is None, setting to 0")
return 0.0
try:
lat_value = float(latitude)
except TypeError:
self.logger.debug(
"Could not convert {0} setting to 0".format(latitude)
)
return 0.0
except ValueError:
self.logger.debug("Latitude is a string {0}".format(latitude))
lat_value = self._convert_position_str2float(latitude)
if abs(lat_value) >= 90:
msg = (
"latitude value = {0} is unacceptable!".format(lat_value)
+ ". Must be |Latitude| > 90"
)
self.logger.error(msg)
raise ValueError(msg)
return lat_value
def _assert_lon_value(self, longitude):
"""
Make sure the longitude value is in decimal degrees, if not change it.
And that the latitude is within -180 < lat > 180.
:param latitude: longitude in decimal degrees or other format
:type latitude: float or string
"""
if longitude in [None, "None", "none", "unknown"]:
self.logger.debug("Longitude is None, setting to 0")
return 0.0
try:
lon_value = float(longitude)
except TypeError:
self.logger.debug(
"Could not convert {0} setting to 0".format(longitude)
)
return 0.0
except ValueError:
self.logger.debug("Longitude is a string {0}".format(longitude))
lon_value = self._convert_position_str2float(longitude)
if abs(lon_value) >= 180:
msg = (
"longitude value = {0} is unacceptable!".format(lon_value)
+ ". Must be |longitude| > 180"
)
self.logger.error(msg)
raise ValueError(msg)
return lon_value
def _assert_elevation_value(self, elevation):
"""
make sure elevation is a floating point number
:param elevation: elevation as a float or string that can convert
:type elevation: float or str
"""
try:
elev_value = float(elevation)
except (ValueError, TypeError):
msg = "Could not convert {0} to a number setting to 0".format(
elevation
)
self.logger.debug(msg)
elev_value = 0.0
return elev_value
def _convert_position_float2str(self, position):
"""
Convert position float to a string in the format of DD:MM:SS.
:param position: decimal degrees of latitude or longitude
:type position: float
:returns: latitude or longitude in format of DD:MM:SS.ms
"""
assert type(position) is float, "Given value is not a float"
deg = int(position)
sign = 1
if deg < 0:
sign = -1
deg = abs(deg)
minutes = (abs(position) - deg) * 60.0
# need to round seconds to 4 decimal places otherwise machine precision
# keeps the 60 second roll over and the string is incorrect.
sec = np.round((minutes - int(minutes)) * 60.0, 4)
if sec >= 60.0:
minutes += 1
sec = 0
if int(minutes) == 60:
deg += 1
minutes = 0
position_str = "{0}:{1:02.0f}:{2:05.2f}".format(
sign * int(deg), int(minutes), sec
)
self.logger.debug("Converted {0} to {1}".format(position, position_str))
return position_str
def _convert_position_str2float(self, position_str):
"""
Convert a position string in the format of DD:MM:SS to decimal degrees
:param position: latitude or longitude om DD:MM:SS.ms
:type position: float
:returns: latitude or longitude as a float
"""
if position_str in [None, "None"]:
return None
p_list = position_str.split(":")
if len(p_list) != 3:
msg = "{0} not correct format, should be DD:MM:SS".format(
position_str
)
self.logger.error(msg)
raise ValueError(msg)
deg = float(p_list[0])
minutes = self._assert_minutes(float(p_list[1]))
sec = self._assert_seconds(float(p_list[2]))
# get the sign of the position so that when all are added together the
# position is in the correct place
sign = 1
if deg < 0:
sign = -1
position_value = sign * (abs(deg) + minutes / 60.0 + sec / 3600.0)
self.logger.debug(
"Converted {0} to {1}".format(position_str, position_value)
)
return position_value
def _assert_minutes(self, minutes):
if not 0 <= minutes < 60.0:
msg = (
"minutes should be 0 < > 60, currently {0:.0f}".format(minutes)
+ " conversion will account for non-uniform"
+ "timne. Be sure to check accuracy."
)
self.logger.warning(msg)
return minutes
def _assert_seconds(self, seconds):
if not 0 <= seconds < 60.0:
msg = (
"seconds should be 0 < > 60, currently {0:.0f}".format(seconds)
+ " conversion will account for non-uniform"
+ "timne. Be sure to check accuracy."
)
self.logger.warning(msg)
return seconds
[docs] def read_dict(self, input_dict):
"""
:param input_dict: DESCRIPTION
:type input_dict: TYPE
:return: DESCRIPTION
:rtype: TYPE
"""
helpers._read_element(self, input_dict, "location")
helpers._read_element(self, input_dict["location"], "declination")
[docs] def to_xml(self, string=False, required=True):
"""
Overwrite to XML to follow EMTF XML format
:param string: DESCRIPTION, defaults to False
:type string: TYPE, optional
:param required: DESCRIPTION, defaults to True
:type required: TYPE, optional
:return: DESCRIPTION
:rtype: TYPE
"""
if self.datum is None:
self.datum = "WGS84"
if self.declination.epoch is None:
self.declination.epoch = "1995"
root = et.Element(
self.__class__.__name__.capitalize(), {"datum": self.datum}
)
lat = et.SubElement(root, "Latitude")
lat.text = f"{self.latitude:.6f}"
lon = et.SubElement(root, "Longitude")
lon.text = f"{self.longitude:.6f}"
elev = et.SubElement(root, "Elevation", {"units": "meters"})
elev.text = f"{self.elevation:.3f}"
dec = et.SubElement(
root, "Declination", {"epoch": self.declination.epoch}
)
dec.text = f"{self.declination.value:.3f}"
if not string:
return root
else:
return element_to_string(root)