#!/usr/bin/env python
##################### IMPORT STANDARD MODULES #########################
# python 3 compatibility
from __future__ import absolute_import, division, print_function
import sys
if (sys.version_info[:2] < (3, 0)):
from builtins import float,int
import numpy as np #for numerical analysis
import typing as tp
####################### IMPORT AVNI LIBRARIES ###########################
from ..tools.common import convert2nparray
from .. import constants
##########################################################################
[docs]def intersection(path1start: tp.Union[list,np.ndarray],
path1brngEnd: tp.Union[float,int,list,np.ndarray],
path2start: tp.Union[list,np.ndarray],
path2brngEnd: tp.Union[float,int,list,np.ndarray]):
"""Get the intersection of two great circle paths. Input can either be
point and bearings or two sets of points.
If c1 & c2 are great circles through start and end points
(or defined by start point + bearing),
then candidate intersections are simply c1 X c2 & c2 x c1
most of the work is deciding correct intersection point to select!
If bearing is given, that determines which intersection,
if both paths are defined by start/end points, take closer intersection
https://www.movable-type.co.uk/scripts/latlong-vectors.html#intersection
Parameters
----------
path1start : tp.Union[list,np.ndarray]
Location of start point of the first curve in [latitude, longitude]
path1brngEnd : tp.Union[float,int,list,np.ndarray]
End point of first curve either in terms of a bearing (float/int) or location [latitude, longitude]
path2start : tp.Union[float,int,list,np.ndarray]
Location of start point of the second curve in [latitude, longitude]
path2brngEnd : tp.Union[float,int,list,np.ndarray]
End point of second curve either in terms of a bearing (float/int) or location [latitude, longitude]
Returns
-------
intersection
Preferred intersection of the two curves based on the input
antipode
Antipode of the intersection where the great-circle curves meet as well
:Authors:
Raj Moulik (moulik@caa.columbia.edu)
:Last Modified:
2023.02.16 5.00
"""
# find out what the types are
# c1 & c2 are vectors defining great circles through start & end points
# p X c gives initial bearing vector
if isinstance(path1brngEnd, (float,int)):
path1def = 'bearing' # path 1 defined by endpoint
p1end = spher2cart(getDestination(path1start[0],path1start[1],path1brngEnd,180.))
else:
path1def = 'endpoint' #path 1 defined by initial bearing
p1end = path1brngEnd
if isinstance(path2brngEnd, (float,int)):
path2def = 'bearing'
p2end = spher2cart(getDestination(path2start[0],path2start[1],path2brngEnd,180.))
else:
path2def = 'endpoint'
p2end = path2brngEnd
case = path1def + '+' + path2def
# convert to spherical coordinates
p1 = spher2cart(path1start)
p2 = spher2cart(path2start)
# Get normal to planes containing great circles
# np.cross product of vector to each point from the origin
N1 = np.cross(p1, p1end)
N2 = np.cross(p2, p2end)
# Find line of intersection between two planes
L = np.cross(N1, N2)
# Find two intersection points
X1 = L / np.sqrt(L[0]**2 + L[1]**2 + L[2]**2)
X2 = -X1
#Check if correct
if np.any(np.isnan(X1)): raise ValueError('not found an intersection point between ',path1start,' with azimuth ',path1brngEnd,' and ', path2start, ' with azimuth ',path2brngEnd)
# convert back to spherical
i1 = cart2spher(X1)[1:]
i2 = cart2spher(X2)[1:]
# selection of intersection point depends on
# how paths are defined (bearings or endpoints)
if case == 'bearing+bearing':
#if NXp1.i1 is +ve, the initial bearing is towards i1
# otherwise towards antipodal i2
dir1 = np.sign(np.dot(np.cross(N1,p1),X1)) #c1Xp1.X1 +ve means p1 bearing points to X1
dir2 = np.sign(np.dot(np.cross(N2,p2),X1)) #c2Xp2.X1 +ve means p2 bearing points to X1
if dir1 + dir2 == 2: # dir1, dir2 both +ve, 1 & 2 both pointing to X1
intersect = i1
antipode = i2
elif dir1 + dir2 == -2: #dir1, dir2 both -ve, 1 & 2 both pointing to X2
intersect = i2
antipode = i1
elif dir1 + dir2 == 0:
# dir1, dir2 opposite; intersection is at further-away intersection point
# take opposite intersection from mid-point of p1 & p2 [is this always true?]
if np.dot(p1+p2,X1) > 0 :
intersect = i2
antipode = i1
else:
intersect = i1
antipode = i2
elif case == 'bearing+endpoint': #use bearing c1 X p1
dir1 = np.sign(np.dot(np.cross(N1,p1),X1)) #c1Xp1.X1 +ve means p1 bearing points to X1
if dir1 > 0:
intersect = i1
antipode = i2
else:
intersect = i2
antipode = i1
elif case == 'endpoint+bearing': #use bearing c2 X p2
dir2 = np.sign(np.dot(np.cross(N2,p2),X1)) #c2Xp2.X1 +ve means p2 bearing points to X1
if dir2 > 0:
intersect = i1
antipode = i2
else:
intersect = i2
antipode = i1
elif case == 'endpoint+endpoint': #select nearest intersection to mid-point of all points
mid = p1+p2+p1end+p2end
if np.dot(mid,X1) > 0 :
intersect = i1
antipode = i2
else:
intersect = i2
antipode = i1
## Attempted with pygeodesy. Has issues for certain configurations.
# if path1start[1] > 180.: path1start[1] = path1start[1] -360.
# path1start = LatLon(path1start[0],path1start[1])
# if path2start[1] > 180.: path2start[1] = path2start[1] -360.
# path2start = LatLon(path2start[0],path2start[1])
# # find out what the types are
# # c1 & c2 are vectors defining great circles through start & end points
# # p X c gives initial bearing vector
# if isinstance(path1brngEnd, (float,int)):
# path1def = 'bearing' # path 1 defined by endpoint
# else:
# path1def = 'endpoint' #path 1 defined by initial bearing
# if path1brngEnd[1] > 180.: path1brngEnd[1] = path1brngEnd[1] -360.
# path1brngEnd = LatLon(path1brngEnd[0],path1brngEnd[1])
# if isinstance(path2brngEnd, (float,int)):
# path2def = 'bearing'
# else:
# path2def = 'endpoint'
# if path2brngEnd[1] > 180.: path2brngEnd[1] = path2brngEnd[1] -360.
# path2brngEnd = LatLon(path2brngEnd[0],path2brngEnd[1])
# intersect = path1start.intersection(path1brngEnd, path2start, path2brngEnd)
return intersect,antipode
[docs]def midpoint(lat1: tp.Union[int,float], lon1: tp.Union[int,float],
lat2: tp.Union[int,float], lon2: tp.Union[int,float]):
"""Get the mid-point from positions in geographic coordinates. Input values as degrees"""
from pygeodesy.sphericalNvector import LatLon
if lon1 > 180.: lon1 = lon1 - 360.
if lon2 > 180.: lon2 = lon2 - 360.
start = LatLon(lat1,lon1)
end = LatLon(lat2,lon2)
mid = start.midpointTo(end)
return [mid.lat, mid.lon]
[docs]def cart2spher(xyz: tp.Union[list,np.ndarray]) -> np.ndarray:
"""Convert from cartesian to spherical coordinates.
Parameters
----------
xyz : tp.Union[list,np.ndarray]
Data containing columns for x,y,z locations in cartesian coordinates
Returns
-------
np.ndarray
Output containing radius, latitude and longitude in spherical coordinates
:Authors:
Raj Moulik (moulik@caa.columbia.edu)
:Last Modified:
2023.02.16 5.00
"""
xyz = convert2nparray(xyz)
rlatlon = np.zeros(xyz.shape)
if xyz.ndim == 2:
xy = xyz[:,0]**2 + xyz[:,1]**2
rlatlon[:,0] = np.sqrt(xy + xyz[:,2]**2)
# ptsnew[:,4] = np.arctan2(np.sqrt(xy), xyz[:,2]) # for elevation angle defined from Z-axis down
#ptsnew[:,4] = np.arctan2(xyz[:,2], np.sqrt(xy)) # for elevation angle defined from XY-plane up
rlatlon[:,1] = np.arctan2(xyz[:,2], np.sqrt(xy))/np.pi*180. # latitude
rlatlon[:,2] = np.arctan2(xyz[:,1], xyz[:,0])/np.pi*180.
elif xyz.ndim == 1:
xy = xyz[0]**2 + xyz[1]**2
rlatlon[0] = np.sqrt(xy + xyz[2]**2)
rlatlon[1] = np.arctan2(xyz[2], np.sqrt(xy))/np.pi*180. # latitude
rlatlon[2] = np.arctan2(xyz[1], xyz[0])/np.pi*180.
else:
raise ValueError('dimension of xyz should be 1 or 2')
return rlatlon
[docs]def spher2cart(rlatlon: tp.Union[list,np.ndarray]) -> np.ndarray:
"""Convert from spherical to cartesian coordinates.
Parameters
----------
rlatlon : tp.Union[list,np.ndarray]
Data containing columns for x,y,z locations in spherical coordinates
Returns
-------
np.ndarray
Output containing x,y,z in spherical coordinates
:Authors:
Raj Moulik (moulik@caa.columbia.edu)
:Last Modified:
2023.02.16 5.00
"""
rlatlon = convert2nparray(rlatlon)
xyz = np.zeros(rlatlon.shape)
if rlatlon.ndim == 2:
# assumes unit sphere if r is not provided
if rlatlon.shape[1] == 2: rlatlon = np.insert(rlatlon,0,1.,axis=1)
colatitude=90.-rlatlon[:,1]
xyz[:,0] = rlatlon[:,0]*np.cos(np.pi/180.*rlatlon[:,2])*np.sin(np.pi/180.*colatitude)
xyz[:,1] = rlatlon[:,0]*np.sin(np.pi/180.*rlatlon[:,2])*np.sin(np.pi/180.*colatitude)
xyz[:,2] = rlatlon[:,0]*np.cos(np.pi/180.*colatitude)
elif rlatlon.ndim == 1:
# assumes unit sphere if r is not provided
if len(rlatlon) == 2: rlatlon = np.insert(rlatlon,0,1.)
colatitude=90.-rlatlon[1]
xyz[0] = rlatlon[0]*np.cos(np.pi/180.*rlatlon[2])*np.sin(np.pi/180.*colatitude)
xyz[1] = rlatlon[0]*np.sin(np.pi/180.*rlatlon[2])*np.sin(np.pi/180.*colatitude)
xyz[2] = rlatlon[0]*np.cos(np.pi/180.*colatitude)
else:
raise ValueError('dimension of rlatlon should be 1 or 2')
return xyz
[docs]def polar2cart(rtheta: tp.Union[list,np.ndarray]) -> np.ndarray:
"""Convert from polar to cartesian coordinates
Parameters
----------
rtheta : tp.Union[list,np.ndarray]
A single r,theta or an array of these for conversion
Returns
-------
np.ndarray
Output containing x,y in cartesian coordinates
:Authors:
Raj Moulik (moulik@caa.columbia.edu)
:Last Modified:
2023.02.16 5.00
"""
rtheta = convert2nparray(rtheta)
xy = np.zeros(rtheta.shape)
if rtheta.ndim == 2:
xy[:,0] = rtheta[:,0]*np.cos(np.pi/180.*rtheta[:,1])
xy[:,1] = rtheta[:,0]*np.sin(np.pi/180.*rtheta[:,1])
elif rtheta.ndim == 1:
xy[0] = rtheta[0]*np.cos(np.pi/180.*rtheta[1])
xy[1] = rtheta[0]*np.sin(np.pi/180.*rtheta[1])
else:
raise ValueError('dimension of rtheta should be 1 or 2')
return xy
[docs]def cart2polar(xy: tp.Union[list,np.ndarray]) -> np.ndarray:
"""Convert from polar to cartesian coordinates
Parameters
----------
xy : tp.Union[list,np.ndarray]
A single x,y or an array of these ffor conversion
Returns
-------
np.ndarray
Output containing r,theta in polar coordinates
:Authors:
Raj Moulik (moulik@caa.columbia.edu)
:Last Modified:
2023.02.16 5.00
"""
xy = convert2nparray(xy)
rtheta = np.zeros(xy.shape)
if xy.ndim == 2:
rtheta[:,0] = np.sqrt(np.power(xy[:,1],2)+np.power(xy[:,1],2))
rtheta[:,1] = np.arctan2(xy[:,1],xy[:,0]) * 180 / np.pi
elif xy.ndim == 1:
rtheta[0] = np.sqrt(np.power(xy[1],2)+np.power(xy[1],2))
rtheta[1] = np.arctan2(xy[1],xy[0]) * 180 / np.pi
else:
raise ValueError('dimension of xy should be 1 or 2')
return rtheta
[docs]def getDestination(lat: tp.Union[int,float],lon: tp.Union[int,float],
azimuth: tp.Union[int,float], distance) -> list:
"""Returns the location of destination point
given the start lat, long, aziuth, and distance (in meters).
Parameters
----------
lat, lon : tp.Union[int,float]
Start point latitude and longitude
azimuth : tp.Union[int,float]
Azimuth to destination
distance
Distance to destination (in meters)
Returns
-------
list
List containing latitude and longitude of destination point
"""
''''''
from pygeodesy.sphericalNvector import LatLon
R = constants.R.to_base_units().magnitude #Radius of the Earth in m
if not isinstance(distance, (float,int)): distance = distance.to_base_units().magnitude #Distance m
if lon > 180.: lon = lon -360.
start = LatLon(lat,lon)
end = start.destination(distance,azimuth,R)
return[end.lat, end.lon]
[docs]def calculateBearing(lat1: tp.Union[int,float], lon1: tp.Union[int,float],
lat2: tp.Union[int,float], lon2: tp.Union[int,float]):
"""Calculates the azimuth in degrees from start point to end point.
Parameters
----------
lat1,lon1 : tp.Union[int,float]
Start point latitude and longitude
lat2,lon2 : tp.Union[int,float]
End point latitude and longitude
Returns
-------
bearing
Bearing to second point
:Authors:
Raj Moulik (moulik@caa.columbia.edu)
:Last Modified:
2023.02.16 5.00
"""
from pygeodesy.sphericalNvector import LatLon
if lon1 > 180.: lon1 = lon1 -360.
start = LatLon(lat1,lon1)
if lon2 > 180.: lon2 = lon2 -360.
end = LatLon(lat2,lon2)
bearing = start.initialBearingTo(end)
return bearing
[docs]def calculateDistance(lat1: tp.Union[int,float],lon1: tp.Union[int,float],
lat2: tp.Union[int,float],lon2: tp.Union[int,float],
final_units='m') -> float:
"""Calculates distance between two coordinates
Parameters
----------
lat1,lon1 : tp.Union[int,float]
Start point latitude and longitude
lat2,lon2 : tp.Union[int,float]
End point latitude and longitude
final_units : str, optional
Output distance in km/m/deg, by default 'm'
Returns
-------
float
Distance between coordinate pairs in m, km or deg
"""
from pygeodesy.sphericalNvector import LatLon
if lon1 > 180.: lon1 = lon1 -360.
if lon2 > 180.: lon2 = lon2 -360.
start = LatLon(lat1,lon1)
end = LatLon(lat2,lon2)
dist = start.distanceTo(end)
if final_units=='m':
return dist
elif final_units=='km':
gcdist = dist / 1000.
return gcdist
elif final_units=='deg':
gcdist = dist / constants.deg2m
return gcdist
else:
raise ValueError('Final units need to be m, km or deg')