# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------
from __future__ import annotations
import math
import numpy as num
from pyrocko import orthodrome
from pyrocko.guts import Float, Object
guts_prefix = 'pf'
d2r = math.pi / 180.
r2d = 1.0 / d2r
km = 1000.
def latlondepth_to_cartesian(lat, lon, depth):
return orthodrome.geodetic_to_ecef(lat, lon, -depth)
[docs]class Location(Object):
'''
Geographical location.
The location is given by a reference point at the earth's surface
(:py:attr:`lat`, :py:attr:`lon`, :py:attr:`elevation`) and a cartesian
offset from this point (:py:attr:`north_shift`, :py:attr:`east_shift`,
:py:attr:`depth`). The offset corrected lat/lon coordinates of the location
can be accessed though the :py:attr:`effective_latlon`,
:py:attr:`effective_lat`, and :py:attr:`effective_lon` properties.
'''
lat = Float.T(
default=0.0,
optional=True,
help='latitude of reference point [deg]')
lon = Float.T(
default=0.0,
optional=True,
help='longitude of reference point [deg]')
north_shift = Float.T(
default=0.,
optional=True,
help='northward cartesian offset from reference point [m]')
east_shift = Float.T(
default=0.,
optional=True,
help='eastward cartesian offset from reference point [m]')
elevation = Float.T(
default=0.0,
optional=True,
help='surface elevation, above sea level [m]')
depth = Float.T(
default=0.0,
help='depth, below surface [m]')
def __init__(self, **kwargs):
Object.__init__(self, **kwargs)
self._latlon = None
def __setattr__(self, name, value):
if name in ('lat', 'lon', 'north_shift', 'east_shift'):
self.__dict__['_latlon'] = None
Object.__setattr__(self, name, value)
@property
def effective_latlon(self):
'''
Property holding the offset-corrected lat/lon pair of the location.
'''
if self._latlon is None:
if self.north_shift == 0.0 and self.east_shift == 0.0:
self._latlon = self.lat, self.lon
else:
self._latlon = tuple(float(x) for x in orthodrome.ne_to_latlon(
self.lat, self.lon, self.north_shift, self.east_shift))
return self._latlon
@property
def effective_lat(self):
'''
Property holding the offset-corrected latitude of the location.
'''
return self.effective_latlon[0]
@property
def effective_lon(self):
'''
Property holding the offset-corrected longitude of the location.
'''
return self.effective_latlon[1]
[docs] def same_origin(self, other):
'''
Check whether other location object has the same reference location.
'''
return self.lat == other.lat and self.lon == other.lon
[docs] def distance_to(self, other):
'''
Compute surface distance [m] to other location object.
:param other: Other location.
:type other: :py:class:`~pyrocko.model.location.Location`
:return: Distance to another location in [m].
:rtype: float
'''
if self.same_origin(other):
other_north_shift, other_east_shift = get_offset(other)
return math.sqrt((self.north_shift - other_north_shift)**2 +
(self.east_shift - other_east_shift)**2)
else:
slat, slon = self.effective_latlon
rlat, rlon = get_effective_latlon(other)
return float(orthodrome.distance_accurate50m_numpy(
slat, slon, rlat, rlon)[0])
[docs] def distance_3d_to(self, other):
'''
Compute 3D distance [m] to other location object.
All coordinates are transformed to cartesian coordinates if necessary
then distance is:
.. math::
\\Delta = \\sqrt{\\Delta {\\bf x}^2 + \\Delta {\\bf y}^2 + \
\\Delta {\\bf z}^2}
:param other: Other location.
:type other: :py:class:`~pyrocko.model.location.Location`
:return: 3D distance to another location in [m].
:rtype: float
'''
if self.same_origin(other):
other_north_shift, other_east_shift = get_offset(other)
return math.sqrt((self.north_shift - other_north_shift)**2 +
(self.east_shift - other_east_shift)**2 +
(self.depth - other.depth)**2)
else:
slat, slon = self.effective_latlon
rlat, rlon = get_effective_latlon(other)
sx, sy, sz = latlondepth_to_cartesian(slat, slon, self.depth)
rx, ry, rz = latlondepth_to_cartesian(rlat, rlon, other.depth)
return math.sqrt((sx-rx)**2 + (sy-ry)**2 + (sz-rz)**2)
[docs] def crosstrack_distance_to(self, path_begin, path_end):
'''
Compute distance to a great-circle arc.
:param path_begin: Location of the start of the arc.
:param path_end: Location of the end of the arc.
:type path_begin: :py:class:`~pyrocko.model.location.Location`
:type path_end: :py:class:`~pyrocko.model.location.Location`
:return: Distance to a great circle arc in [deg].
:rtype: float
'''
return orthodrome.crosstrack_distance(
*path_begin.effective_latlon,
*path_end.effective_latlon,
*self.effective_latlon
)
[docs] def alongtrack_distance_to(self, path_begin, path_end, meter=False):
'''
Compute distance along a great-circle arc.
:param path_begin: Location of the start of the arc.
:param path_end: Location of the end of the arc.
:param meter: Return [m] instead of [deg].
:type path_begin: :py:class:`~pyrocko.model.location.Location`
:type path_end: :py:class:`~pyrocko.model.location.Location`
:return: Distance from the start of the great circle arc
in [deg] or [m].
:rtype: float
'''
if meter:
return orthodrome.alongtrack_distance_m(
*path_begin.effective_latlon,
*path_end.effective_latlon,
*self.effective_latlon
)
return orthodrome.alongtrack_distance(
*path_begin.effective_latlon,
*path_end.effective_latlon,
*self.effective_latlon
)
def offset_to(self, other):
if self.same_origin(other):
other_north_shift, other_east_shift = get_offset(other)
return (
other_north_shift - self.north_shift,
other_east_shift - self.east_shift)
else:
azi, bazi = self.azibazi_to(other)
dist = self.distance_to(other)
return dist*math.cos(azi*d2r), dist*math.sin(azi*d2r)
[docs] def azibazi_to(self, other):
'''
Compute azimuth and backazimuth to and from other location object.
:param other: Other location.
:type other: :py:class:`~pyrocko.model.location.Location`
:return: Azimuth and back-azimuth to the location in [deg].
:rtype: tuple[float, float]
'''
if self.same_origin(other):
other_north_shift, other_east_shift = get_offset(other)
azi = r2d * math.atan2(other_east_shift - self.east_shift,
other_north_shift - self.north_shift)
bazi = azi + 180.
else:
slat, slon = self.effective_latlon
rlat, rlon = get_effective_latlon(other)
azi, bazi = orthodrome.azibazi_numpy(slat, slon, rlat, rlon)
return float(azi), float(bazi)
def set_origin(self, lat, lon):
lat = float(lat)
lon = float(lon)
elat, elon = self.effective_latlon
n, e = orthodrome.latlon_to_ne_numpy(lat, lon, elat, elon)
self.lat = lat
self.lon = lon
self.north_shift = float(n)
self.east_shift = float(e)
self._latlon = elat, elon # unchanged
@property
def coords5(self):
return num.array([
self.lat, self.lon, self.north_shift, self.east_shift, self.depth])
[docs]def filter_azimuths(
locations: list[Location],
center: Location,
azimuth: float,
azimuth_width: float,
) -> list[Location]:
"""Filter locations by azimuth swath.
Args:
locations (list[Location]): List of Locations.
center (Location): Relative center location.
azimuth (float): Azimuth in [deg]. -180 to 180 or 0 to 360.
azimuth_width (float): Width of the swath.
Returns:
list[Location]: Filtered locations.
"""
OFFSET = 720.0
filt_locations = []
azimuth_min = azimuth + OFFSET - azimuth_width
azimuth_max = azimuth + OFFSET + azimuth_width
for loc in locations:
azimuth, _ = center.azibazi_to(loc)
if (azimuth_min <= azimuth + OFFSET <= azimuth_max) or (
azimuth_min <= azimuth % 360.0 + OFFSET <= azimuth_max
):
filt_locations.append(loc)
return filt_locations
[docs]def filter_distance(
locations: list[Location],
reference: Location,
distance_min: float,
distance_max: float,
) -> list[Location]:
"""Filter location by distance to a reference point.
Args:
locations (list[Location]): Locations to filter.
reference (Location): Reference location.
distance_min (float): Minimum distance in [m].
distance_max (float): Maximum distance in [m].
Returns:
list[Location]: Filtered locations.
"""
return [
loc
for loc in locations
if distance_min <= loc.distance_to(reference) <= distance_max
]
[docs]def filter_crosstrack_distance(
locations: list[Location],
start_path: Location,
end_path: Location,
distance_max: float
) -> list[Location]:
"""Filter location by distance to a great-circle path.
Args:
locations (list[Location]): Locations to filter.
start_path (Location): Start of the great circle path.
end_path (Location): End of the great circle path.
distance_max (float): Distance to the great-circle in [deg].
Returns:
list[Location]: Filtered locations.
"""
return [
loc
for loc in locations
if abs(loc.crosstrack_distance_to(
start_path, end_path)) <= distance_max
]
def get_offset(obj):
try:
return obj.north_shift, obj.east_shift
except AttributeError:
return 0.0, 0.0
def get_effective_latlon(obj):
try:
return obj.effective_latlon
except AttributeError:
return obj.lat, obj.lon