navigation
– Navigation Calculations¶
The navtools.navigation
module computes range and bearing between two points.
It leverages the navtools.igrf
module to compute compass bearing from
true bearing.
See the Aviation Formulary: http://edwilliams.org/avform147.htm for a number of useful formulae and examples.
Also see http://www.movable-type.co.uk/scripts/latlong.html, © 2002-2010 Chris Veness.
Distance/Bearing Calculation¶
These are based on the equirectangular approximation for distance. This the loxodrome or rhumb line.
See http://edwilliams.org/avform147.htm#flat.
Generally, the \(\phi\) values are N-S latitude, and \(\lambda\) values are E-W longitude.
This means \((\phi_0, \lambda_0)\) and \((\phi_1, \lambda_1)\) are the two points we’re navigating between.
The distance, \(d\), is given by the following computation:
We could fine-tune this with \(R_y\) and \(R_x\) radius of curvature values. We don’t need answers closer than 10%, so we skip this in the implementation.
The implementation does not compute the \(R_y\) flattening effect on the north-south component of a distance; nor does it compute the \(R_x\) flattening effect on east-west component of a distance.
Where
a
is the equatorial radius of the earth:
a=6378.137000 km for WGS84
a=3958.761 miles
a=3440.069 nautical miles, but 180*60/pi can be more useful.
e^2=f*(2-f)
is the eccentricity, a function of the flattening factor, f=1/298.257223563, for WGS84.
See https://en.wikipedia.org/wiki/World_Geodetic_System for more details.
The bearing, \(\theta\) is given by this formula.
Example¶
Suppose point 1 is LAX: (33deg 57min N, 118deg 24min W)
Suppose point 2 is JFK: (40deg 38min N, 73deg 47min W)
d = 0.629650 radians = 2164.6 nm theta = 1.384464 radians = 79.32 degrees
Conversely, 2164.6nm (0.629650 radians) on a rhumbline course of 79.3 degrees (1.384464 radians) starting at LAX should arrive at JFK.
Destination Calculation¶
This is the direct Rhumb-line formula. From \((\phi_1, \lambda_1)\) in direction \(\theta\) for distance \(d\).
Generally, the \(\phi\) values are N-S latitude, and \(\lambda\) values are E-W longitude.
The steps:
Compute new latitude.
\[\phi_d = \phi_1 + d \cos \theta\]Sanity Check.
\[\lvert \phi_d \rvert \leq \frac{\pi}{2}\]How much northing? Too little? We’re on an E-W line and a simplification avoids a fraction with terms near zero. A lot? We’re not an a simple E-W line.
\[\begin{split}q = \begin{cases} \cos \phi_1 &\textbf{if $\lvert \phi_d - \phi_1 \rvert < \sqrt{\epsilon}$} \\ \phi_d - \phi_1 / \log {\dfrac{\tan(\tfrac{\phi_d}{2}+\tfrac{\pi}{4})}{\tan(\tfrac{\phi_1}{2}+\tfrac{\pi}{4})}} &\textbf{otherwise} \end{cases}\end{split}\]Compute new Longitude
\[\begin{split}\Delta \lambda &= d \times \frac{\sin(\theta)}{q} \\ \lambda_d &= (\lambda_1 + \Delta \lambda + \pi \mod 2\pi) - \pi\end{split}\]
Implementation¶
Here’s the UML overview of this module.
AngleParser¶
- class navtools.navigation.AngleParser¶
Parse a sting representation of a latitude or longitude.
>>> AngleParser.parse("76.123N") 76.123 >>> AngleParser.parse("76.123S") -76.123 >>> AngleParser.parse("Due North") Traceback (most recent call last): ... ValueError: Cannot parse 'Due North'
- dms_pat = re.compile('(\\d+)\\D+(\\d+)\\D+(\\d+)[^NEWSnews]*([NEWSnews]?)')¶
- dm_pat = re.compile('(\\d+)\\D+(\\d+\\.\\d+)[^NEWSnews]*([NEWSnews]?)')¶
- d_pat = re.compile('(\\d+\\.\\d+)[^NEWSnews]*([NEWSnews]?)')¶
- static sign(txt: str) → int¶
- static parse(value: str) → float¶
Parses a text value, returning signed degrees as a float value.
- Parameters
value – text to parse
- Returns
float degrees or a
ValueError
exception.
Angle class hierarchy¶
The superclass, navtools.navigation.Angle
.
- class navtools.navigation.Angle(x=0, /)¶
A signed angle in radians; the superclass for Lat and Lon. These are relative to the equator or the prime meridian. In that sense, they aren’t completely generic angles; they’re restricted in their meaning.
Currently, we extend
float
. This leads to the followingDeprecationWarning: Angle.__float__ returned non-float (type Angle). The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.
This suggests we need to switch to a separate class that implements
numbers.Real
. This would “wrap” the internalfloat
object.class Angle(numbers.Real): def __init__(self, value: float) -> None: self.value = value def __add__(self, other: Any) -> Angle: return self.value + cast(Angle, other).value etc.
This class has lots of conversions to DMS. A subclass can treat the sign (“h”) as the hemisphere, using “N”, “S”, “E”, or “W”. Lat uses “N”/”S”, Lon uses “E”/”W”.
Note that we have to “prettify” some values to remove annoying 10E-16 noise bits that arise sometimes.
>>> import math >>> a = Angle(-math.pi/6) >>> round(a,3) -0.524 >>> round(a.radians,3) -0.524 >>> round(a.degrees,3) -30.0 >>> round(a.sdeg,3) -30.0 >>> round(a.deg,3) -30.0 >>> a.dm (-30, 0.0) >>> a.dms (-30, 0, 0.0) >>> a.h '-' >>> round(a.r,3) -0.524
Formatter.
>>> fmt, prop = Angle._rewrite("%02.0d° %2.5m'") >>> fmt "{d:02.0f}° {m:2.5f}'" >>> sorted(prop) ['d', 'm'] >>> "Lat: {0:%02.0d° %6.3m'}".format(a) "Lat: 30° 0.000'"
Another round-off test.
>>> a2 = Angle(math.pi/12) >>> a2.dms (15, 0, 0.0)
Math.
>>> round(a+a2, 5) -0.2618 >>> round((a+a2).degrees, 3) -15.0
We define the core numeric object special methods, all of which simply appeal to the superclass methods for the implementation. The results create new
Angle
objects, otherwise, we behave just like afloat
.- parser¶
alias of
navtools.navigation.AngleParser
- classmethod fromdegrees(deg: float, hemisphere: Optional[str] = None) → navtools.navigation.Angle¶
Creates an
Angle
from a numeric value, which must be degrees.- Parameters
deg – numeric degrees value.
hemisphere – sign value, which can be encoded as “N”, “S”, “E”, or “W”. If omitted, it’s positive.
- Returns
Angle
object.
>>> a = Angle.fromdegrees(45) >>> round(a, 4) 0.7854 >>> b = Angle.fromdegrees(23.456, "N") >>> round(b, 4) 0.4094
- classmethod fromdegmin(deg: float, min: float, hemisphere: Optional[str] = None) → navtools.navigation.Angle¶
- classmethod fromstring(value: str) → navtools.navigation.Angle¶
Creates an
Angle
from a string value, which must be represent degrees, either as a simple string value, or as a more complex value recognized by theAngleParser
class.- Parameters
deg – string degrees value.
hemisphere – sign value, which can be encoded as “N”, “S”, “E”, or “W”. If omitted, it’s positive.
- Returns
Angle
object.
We start by assuming the text value is simply a string representation of a float. If it isn’t, we use the
AngleParser
class to parse the string.A subclass might change the
parser
reference to use a different parser.>>> a0 = Angle.fromstring("-77.4325") >>> round(a0.degrees,4) -77.4325 >>> a1 = Angle.fromstring("37°28'8\"N") >>> round(a1.degrees,4) 37.4689
Note use of prime and double-prime which are preferred over ‘ and “
>>> a2 = Angle.fromstring("77°25′57″W") >>> round(a2.degrees,4) -77.4325
- classmethod parse(value: str) → navtools.navigation.Angle¶
Alias for
fromstring()
- property radians: float¶
Angle in radians
- property r: float¶
Angle in radians
- property degrees: float¶
Angle in signed degrees
- property sdeg: float¶
Angle in signed degrees
- property deg: float¶
Angle in signed degrees
- property dm: tuple¶
- Returns
(d, m) tuple of signed values
- property dms: tuple¶
- Returns
(d, m, s) tuple of signed values
- property h: str¶
The
h()
property is the sign; the “hemisphere”. A subclass likeLat
andLon
will override this to provide a string instead of an int value.- Returns
“+” or “-”
- spec_pat = re.compile('%([0-9\\.#\\+ -]*)([dmshr])')¶
- formatter = <string.Formatter object>¶
- classmethod _rewrite(spec: str) → tuple¶
Rewrites a “%x” spec into a “{x:fmt}” format. Returns the revised format at the set of properties used.
There are several variant cases where we want different kinds of display values:
%d %m %s
means that degrees and minutes are integer values.%d %m
means that degrees is an int and m is a float.%d
means that degrees is a float.
To make this work, we note the pattern of
{'d', 'm', 's'}
, or{'d', 'm'}
, or{'d'}
and determine the appropriate mix of int or float values to include.
The Latitude subclass, navtools.navigation.Lat
.
- class navtools.navigation.Lat(x=0, /)¶
Latitude Angle, normal to the equator.
Hemisphere text of “N” and “S”. Two digits as the default format for degrees.
>>> a = Lat.fromdegrees(37.1234) >>> repr(a) '37°07.404′N' >>> b = Lat.fromstring('37°07.404′N') >>> round(b.degrees,4) 37.1234
- classmethod fromdegrees(deg: float, hemisphere: Optional[str] = None) → navtools.navigation.Lat¶
Creates an
Angle
from a numeric value, which must be degrees.- Parameters
deg – numeric degrees value.
hemisphere – sign value, which can be encoded as “N”, “S”, “E”, or “W”. If omitted, it’s positive.
- Returns
Angle
object.
>>> a = Angle.fromdegrees(45) >>> round(a, 4) 0.7854 >>> b = Angle.fromdegrees(23.456, "N") >>> round(b, 4) 0.4094
- classmethod fromstring(value: str) → navtools.navigation.Lat¶
Creates an
Angle
from a string value, which must be represent degrees, either as a simple string value, or as a more complex value recognized by theAngleParser
class.- Parameters
deg – string degrees value.
hemisphere – sign value, which can be encoded as “N”, “S”, “E”, or “W”. If omitted, it’s positive.
- Returns
Angle
object.
We start by assuming the text value is simply a string representation of a float. If it isn’t, we use the
AngleParser
class to parse the string.A subclass might change the
parser
reference to use a different parser.>>> a0 = Angle.fromstring("-77.4325") >>> round(a0.degrees,4) -77.4325 >>> a1 = Angle.fromstring("37°28'8\"N") >>> round(a1.degrees,4) 37.4689
Note use of prime and double-prime which are preferred over ‘ and “
>>> a2 = Angle.fromstring("77°25′57″W") >>> round(a2.degrees,4) -77.4325
- property d: float¶
- Returns
Latitude in degrees.
- property h: str¶
- Returns
The hemisphere, “N” or “S”.
- property north: float¶
- Returns
North latitude, positive “co-latitude”. Range is 0 to pi instead of -pi/2 to +pi/2.
The Longitude subclass, navtools.navigation.Lon
.
- class navtools.navigation.Lon(x=0, /)¶
Longitude Angle, parallel to the equator
Hemisphere text of “E” and “W”. Three digits as the default format for degrees.
>>> a = Lon.fromdegrees(-76.5678) >>> repr(a) '076°34.068′W' >>> b = Lon.fromstring('076°34.068′W') >>> round(b.degrees,4) -76.5678
- classmethod fromdegrees(deg: float, hemisphere: Optional[str] = None) → navtools.navigation.Lon¶
Creates an
Angle
from a numeric value, which must be degrees.- Parameters
deg – numeric degrees value.
hemisphere – sign value, which can be encoded as “N”, “S”, “E”, or “W”. If omitted, it’s positive.
- Returns
Angle
object.
>>> a = Angle.fromdegrees(45) >>> round(a, 4) 0.7854 >>> b = Angle.fromdegrees(23.456, "N") >>> round(b, 4) 0.4094
- classmethod fromstring(value: str) → navtools.navigation.Lon¶
Creates an
Angle
from a string value, which must be represent degrees, either as a simple string value, or as a more complex value recognized by theAngleParser
class.- Parameters
deg – string degrees value.
hemisphere – sign value, which can be encoded as “N”, “S”, “E”, or “W”. If omitted, it’s positive.
- Returns
Angle
object.
We start by assuming the text value is simply a string representation of a float. If it isn’t, we use the
AngleParser
class to parse the string.A subclass might change the
parser
reference to use a different parser.>>> a0 = Angle.fromstring("-77.4325") >>> round(a0.degrees,4) -77.4325 >>> a1 = Angle.fromstring("37°28'8\"N") >>> round(a1.degrees,4) 37.4689
Note use of prime and double-prime which are preferred over ‘ and “
>>> a2 = Angle.fromstring("77°25′57″W") >>> round(a2.degrees,4) -77.4325
- property d: float¶
- Returns
Longitude in degrees.
- property h: str¶
- Returns
The hemisphere, “E” or “W”.
- property east: float¶
- Returns
East longitude. Positive only.
LatLon point¶
- class navtools.navigation.LatLon(lat: Union[navtools.navigation.Lat, navtools.navigation.Angle, float, str], lon: Union[navtools.navigation.Lon, navtools.navigation.Angle, float, str])¶
A latitude/longitude coordinate pair. This is a glorified namedtuple with additional properties to provide nicely-formatted results.
This includes input and output conversions.
Output conversions as degree-minute-second, degree-minute, and degree. We return a tuple of two strings so that the application can use these values to populate separate spreadsheet columns.
- Variables
- lat_dms_format = '{0:%02.0d %02.0m %04.1s%h}'¶
- lon_dms_format = '{0:%03.0d %02.0m %04.1s%h}'¶
- lat_dm_format = '{0:%02.0d %.3m%h}'¶
- lon_dm_format = '{0:%03.0d %.3m%h}'¶
- lat_d_format = '{0:%06.3d%h}'¶
- lon_d_format = '{0:%07.3d%h}'¶
- property dms: tuple¶
Long Degree Minute Second format.
- Returns
A pair of strings of the form
ddd mm s.sh
- property dm: tuple¶
GPS-friendly Degree Minute format.
- Returns
A pair of strings of the form
ddd m.mmmh
- property d: tuple¶
GPS-friendly Degree format.
- Returns
A pair of strings of the form
ddd.dddh
- near(other: navtools.navigation.LatLon, R: float = 3440.069) → float¶
Distance from another point. This can be expensive to compute, a geocode to do proximity tests can be more efficient.
Globals¶
- navtools.navigation.KM¶
- navtools.navigation.MI¶
- navtools.navigation.NM¶
range and bearing¶
- navtools.navigation.range_bearing(p1: navtools.navigation.LatLon, p2: navtools.navigation.LatLon, R: float = 3440.069) → tuple¶
Rhumb-line course from
p1
top2
.See Distance/Bearing Calculation. This is the equirectangular approximation. Without even the minimal corrections for non-spherical Earth.
destination¶
- navtools.navigation.destination(p1: navtools.navigation.LatLon, range: float, bearing: float, R: float = 3440.069) → navtools.navigation.LatLon¶
Rhumb line destination given point, range and bearing.
- Parameters
- Returns
a
LatLon
with the ending point.
declination (or variance)¶
- navtools.navigation.declination(point: navtools.navigation.LatLon, date: Optional[datetime.date] = None) → float¶
Computes standard declination for a given
LatLon
point.http://www.ngdc.noaa.gov/geomag/models.shtml
http://www.ngdc.noaa.gov/IAGA/vmod/igrf.html
See igrf – International Geomagnetic Reference Field for details.
- Parameters
point – LatLon point
date –
datetime.date
in question, default is today.
- Returns
declination as a float offset to an Angle.
Historical Archive¶
The original Angle
and GlobeAngle
classes do things which are close
to correct. They included some needless complexity, however.
They worked in degrees (not radians) and implemented a lot of operations that could
have been inherited from float
.
Angle class – independent of float¶
An Angle
is a signed radians number, essentially equivalent
to float
. The operators are include the flexibility to work with
float
values, doing coercion to Angle
.
class Angle(numbers.Real):
"""
An Angle, with conversion from several DMS notations,
as well as from radians. The angle can be reported as
degrees, a (D, M, S) tuple or as a value in radians.
:ivar deg: The angle in degrees.
:ivar radians: The angle in radians
:ivar dm: The angle as a (D, M) tuple
:ivar dms: The angle as a (D, M, S) tuple
:ivar tail: Any additional text found after parsing a string value.
This may be a hemisphere indication that a subclass might want to use.
"""
dms_pat= re.compile( r"(\d+)\s+(\d+)\s+(\d+)(.*)" )
dm_pat= re.compile( r"(\d+)\s+(\d+\.\d+)(.*)" )
d_pat= re.compile( r"(\d+\.\d+)(.*)" )
navx_dmh_pat= re.compile( "(\\d+)\\D+(\\d+\\.\\d+)'([NEWS])" )
@staticmethod
def from_radians( value ):
"""Create an Angle from radians.
:param value: Angle in radians.
Generally used like this::
a = Angle.from_radians( float )
"""
return Angle( 180 * value / math.pi )
def __init__( self, value ):
"""Create an Angle from an Angle, float or string degrees.
:param value: Angle in degrees as Angle, float or string.
"""
self.tail= None
if isinstance(value,Angle):
self.deg= value.deg
return
if isinstance(value,float):
self.deg= value
return
dms= Angle.dms_pat.match( value )
if dms:
d, m, s = int(dms.group(1)), int(dms.group(2)), float(dms.group(3))
self.deg= d + m/60 + s/3600
self.tail= dms.group(4)
return
dm= Angle.dm_pat.match( value )
if dm:
d, m = int(dm.group(1)), float(dm.group(2))
self.deg= d + m/60
self.tail= dm.group(3)
return
d= Angle.d_pat.match( value )
if d:
self.deg = float(d.group(1))
self.tail= d.group(2)
return
navx= Angle.navx_dmh_pat.match( value )
if navx:
d, m = float(navx.group(1)), float(navx.group(2))
self.deg= d + m/60
self.tail= navx.group(3)
return
raise TypeError( "Cannot parse Angle {0!r}".format(value) )
@property
def radians( self ):
"""Returns the angle in radians.
:returns: angle in radians.
"""
return math.pi * self.deg / 180
@property
def dm( self ):
"""Returns the angle as (D, M).
:returns: (d, m) tuple
"""
sign= -1 if self.deg < 0 else +1
ad= abs(self.deg)
d= int(ad)
ms= ad-d
return d*sign, 60*ms*(sign if d == 0 else 1)
@property
def dms( self ):
"""Returns the angle as (D, M, S).
:returns: (d, m, s) tuple
"""
sign= -1 if self.deg < 0 else +1
ad= abs(self.deg)
d= int(ad)
ms= 60*(ad-d)
m= int(ms)
s= round((ms-m)*60,3)
return d*sign, m*(sign if d == 0 else 1), s*(sign if d==0 and m==0 else 1)
def __repr__( self ):
return "Angle( {0.deg!r} )".format( self )
def __str__( self ):
return "{0.deg:7.3f}".format( self )
def __add__( self, other ):
if isinstance(other,Angle):
return Angle( self.deg + other.deg )
elif isinstance(other,float):
return Angle( self.deg + other )
else:
return NotImplemented
def __sub__( self, other ):
if isinstance(other,Angle):
return Angle( self.deg - other.deg )
elif isinstance(other,float):
return Angle( self.deg - other )
else:
return NotImplemented
def __mul__( self, other ):
if isinstance(other,Angle):
return Angle( self.deg * other.deg )
elif isinstance(other,float):
return Angle( self.deg * other )
else:
return NotImplemented
def __div__( self, other ):
if isinstance(other,Angle):
return Angle( self.deg / other.deg )
elif isinstance(other,float):
return Angle( self.deg / other )
else:
return NotImplemented
def __truediv__( self, other ):
return self.__div__( self, other )
def __floordiv__( self, other ):
if isinstance(other,Angle):
return Angle( self.deg // other.deg )
elif isinstance(other,float):
return Angle( self.deg // other )
else:
return NotImplemented
def __mod__( self, other ):
if isinstance(other,Angle):
return Angle( self.deg % other.deg )
elif isinstance(other,float):
return Angle( self.deg % other )
else:
return NotImplemented
def __abs__( self ):
return Angle( abs(self.deg) )
def __float__( self ):
return self.deg
def __trunc__( self ):
return Angle( trunc(self.deg) )
def __ceil__( self ):
return Angle( math.ceil( self.deg ) )
def __floor__( self ):
return Angle( math.floor( self.deg ) )
def __round__( self, ndigits ):
return Angle( round( self.deg, ndigits=0 ) )
def __neg__( self ):
return Angle( -self.deg )
def __pos__( self ):
return self
def __eq__( self, other ):
return self.deg == other.deg
def __ne__( self, other ):
return self.deg != other.deg
def __le__( self, other ):
return self.deg <= other.deg
def __lt__( self, other ):
return self.deg < other.deg
def __ge__( self, other ):
return self.deg >= other.deg
def __gt__( self, other ):
return self.deg > other.deg
def __pow__( self, other ):
return Angle( self.deg**other )
def __radd__( self, other ):
return other+self
def __rdiv__( self, other ):
return NotImplemented
def __rfloordiv__( self, other ):
return NotImplemented
def __rmod__( self, other ):
return NotImplemented
def __rmul__( self, other ):
return other*self
def __rpow__( self, other ):
return NotImplemented
def __rtruediv__( self, other ):
return NotImplemented
Old GlobeAngle Class¶
We need to extend the simple Angle
to include globe hemisphere
information so that the simple angle (in degrees or radians)
can be parsed and presented in proper N, S, E and W hemisphere
notation.
Important
This doesn’t handle signs properly.
Note that we do not handle sign well as a conversion from a string. This is because this angle is axis-independent. Since it isn’t aware of being a longitude or a latitude, it doesn’t know which hemisphere code to use.
class GlobeAngle( Angle ):
"""An Angle which includes hemisphere information: N, S, E or W.
:ivar deg: The angle in degrees.
:ivar radians: The angle in radians
:ivar dm: The angle as a (D, M) tuple
:ivar dms: The angle as a (D, M, S, H) tuple
:ivar hemi: The hemisphere ("N", "S", "E" or "W")
"""
def _hemisphere( self, hemi, debug=None ):
if len(hemi) == 1:
self.hemi= hemi
elif len(hemi) == 2:
self.hemi= hemi[0 if self.deg >= 0 else 1]
else:
raise TypeError( "Cannot parse GlobeAngle{0!r}".format(debug) )
def __init__( self, value, hemi=None ):
"""Create a GlobeAngle from an GlobeAngle, Angle or float degrees.
This will delegate construction to Angle for parsing the
various strings that could be present. An Angle string may
include a "tail" of N, S, E or W, making the hemisphere
irrelevant.
:param value: Angle in degrees as :py:class:`Angle`,
:py:class:`GlobeAngle`, float or string.
The string parsing is delegated to :py:class:`Angle`.
:param hemi: The hemisphere label ('N', 'S', 'E' or 'W')
Or.
In the case of Angle or Float, this is the set of hemisphere
alternatives. For Latitude provide "NS"; for Longitude provide "EW".
This must be folded into an Angle or a float value.
Positive Angle or float means N or E.
Negative Angle or float means S or W.
"""
if isinstance(value,GlobeAngle):
self.deg= value.deg
self.hemi= value.hemi
return
if isinstance(value,Angle):
self.deg= value.deg
self._hemisphere( hemi, debug=(value,hemi) )
return
if isinstance(value,float) and hemi is not None:
self.deg= value
self._hemisphere( hemi, debug=(value,hemi) )
return
# String parsing.
angle= Angle( value )
self.deg= angle.deg
if angle.tail and angle.tail[0].upper() in ("N","S","E","W"):
self.hemi= angle.tail[0].upper()
return
self._hemisphere( hemi, debug=(value,hemi) )
@property
def radians( self ):
"""Returns the angle in radians with appropriate sign based on hemisphere.
W and S are negative values.
:returns: angle in radians.
"""
if self.hemi in ("W","S"):
return -super(GlobeAngle,self).radians
return super(GlobeAngle,self).radians
@property
def dms( self ):
"""Returns the angle as a (D, M, S, Hemisphere).
:return: (d, m, s, hemisphere) 4-tuple.
"""
return super(GlobeAngle,self).dms + ( self.hemi, )
@property
def sdeg( self ):
"""Returns a signed angle: positive N or E, negative S or W."""
if self.hemi in ("S", "W"):
return -self.deg
return self.deg
def __str__( self ):
return str(self.deg)+self.hemi
Old Rhumb-Line Range and Bearing¶
This is not what we’re using. This is an alternative that uses a more sophsticated Rhumb line computation. The increased accuracy isn’t important enough to use.
def range_bearing(p1: LatLon, p2: LatLon, R: float = NM) -> Tuple[float, float]:
lat1 = p1.lat.radians
lat2 = p2.lat.radians
dLat = lat2 - lat1
dPhi = math.log(math.tan(lat2/2+math.pi/4)/math.tan(lat1/2+math.pi/4))
if abs(dPhi) < 1.0E-6:
q = math.cos(lat1)
else:
q = dLat/dPhi
lon1 = p1.lon.radians
lon2 = p2.lon.radians
dLon = lon2 - lon1
if abs(dLon) > math.pi:
dLon = -(2*math.pi-dLon) if dLon > 0 else (2*math.pi+dLon)
d = math.sqrt(dLat*dLat + q*q*dLon*dLon) * R
brng = math.atan2(dLon, dPhi)
if brng < 0:
brng = 2*math.pi+brng
theta = Angle(brng)
return d, theta