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:

\[\begin{split}x &= R \times \Delta \lambda \times \cos \frac{\Delta \phi}{2} \\ y &= R \times \Delta \phi \\ d &= \sqrt{x^2 + y^2}\end{split}\]

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.

\[\begin{split}R_y &= a \times \frac{1-e^2}{\bigl(1 - e^2 \times \sin(\phi_0)^2\bigr)^\frac{3}{2}} \\ R_x &= a \times \frac{1}{\sqrt{1 - e^2 \times \sin(\phi_0)^2}}\end{split}\]

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.

\[e^2 = f\times(2-f) = 0.0066943799901413165\]

See https://en.wikipedia.org/wiki/World_Geodetic_System for more details.

The bearing, \(\theta\) is given by this formula.

\[\theta = \arctan \frac{x} {y} \mod 2 \pi\]

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:

  1. Compute new latitude.

    \[\phi_d = \phi_1 + d \cos \theta\]
  2. Sanity Check.

    \[\lvert \phi_d \rvert \leq \frac{\pi}{2}\]
  3. 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}\]
  4. 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.

@startuml
'navtools.navigation'
allow_mixing

component navigation {

    class AngleParser {
        {static} sign(str) : int
        {static} parse(str) : str
    }

    class float

    class Angle {
        {static} fromstring(str) : str
    }

    float <|-- Angle

    Angle --> AngleParser

    class Lat

    class Lon

    Angle <|-- Lat
    Angle <|-- Lon

    class LatLon {
        lat : Lat
        lon : Lon
    }

    LatLon::lat -- Lat
    LatLon::lon -- Lon
}

component igrf

navigation ..> igrf

@enduml

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 following

DeprecationWarning: 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 internal float 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 a float.

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 the AngleParser 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 like Lat and Lon 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 the AngleParser 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 the AngleParser 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 – The latitude Lat.

  • lon – The longitude Lon.

  • dms – A pair of DMS strings.

  • dm – A pair of DM strings.

  • d – A pair of D strings.

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 to p2.

See Distance/Bearing Calculation. This is the equirectangular approximation. Without even the minimal corrections for non-spherical Earth.

Parameters
  • p1 – a LatLon starting point

  • p2 – a LatLon ending point

  • R – radius of the earth in appropriate units; default is nautical miles. Values include KM for kilometers, MI for statute miles and NM for nautical miles.

Returns

2-tuple of range and bearing from p1 to p2.

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.

See Destination Calculation.

Parameters
  • p1 – a LatLon starting point

  • range – the distiance to travel.

  • bearing – the direction of travel in degrees.

  • R – radius of the earth in appropriate units; default is nautical miles. Values include KM for kilometers, MI for statute miles and NM for nautical miles.

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

Navtools

Navigation

  • Overview and Context
  • Actors
  • Use Cases
  • Container Overview
  • Component Architecture
  • Other Notes
  • References
  • planning – Route Planning Application
  • analysis – Track Analysis Application
  • opencpn_table – OpenCPN Table Application
  • waypoint_merge – Waypoint and Route Merge Application
  • navigation – Navigation Calculations
  • lowrance_usr – Lowrance USR File Parser
  • olc – OLC Geocoding
  • igrf – International Geomagnetic Reference Field
  • solar – Sunrise and Sunset

Related Topics

  • Documentation overview
    • Previous: waypoint_merge – Waypoint and Route Merge Application
    • Next: lowrance_usr – Lowrance USR File Parser

Quick search

©2021, S.Lott. | Powered by Sphinx 4.0.2 & Alabaster 0.7.12 | Page source