"""Define the :class:`~geographiclib.geodesicline.GeodesicLine` class
The constructor defines the starting point of the line. Points on the
line are given by
* :meth:`~geographiclib.geodesicline.GeodesicLine.Position` position
given in terms of distance
* :meth:`~geographiclib.geodesicline.GeodesicLine.ArcPosition` position
given in terms of spherical arc length
A reference point 3 can be defined with
* :meth:`~geographiclib.geodesicline.GeodesicLine.SetDistance` set
position of 3 in terms of the distance from the starting point
* :meth:`~geographiclib.geodesicline.GeodesicLine.SetArc` set
position of 3 in terms of the spherical arc length from the starting point
The object can also be constructed by
* :meth:`Geodesic.Line <geographiclib.geodesic.Geodesic.Line>`
* :meth:`Geodesic.DirectLine <geographiclib.geodesic.Geodesic.DirectLine>`
* :meth:`Geodesic.ArcDirectLine
<geographiclib.geodesic.Geodesic.ArcDirectLine>`
* :meth:`Geodesic.InverseLine <geographiclib.geodesic.Geodesic.InverseLine>`
The public attributes for this class are
* :attr:`~geographiclib.geodesicline.GeodesicLine.a`
:attr:`~geographiclib.geodesicline.GeodesicLine.f`
:attr:`~geographiclib.geodesicline.GeodesicLine.caps`
:attr:`~geographiclib.geodesicline.GeodesicLine.lat1`
:attr:`~geographiclib.geodesicline.GeodesicLine.lon1`
:attr:`~geographiclib.geodesicline.GeodesicLine.azi1`
:attr:`~geographiclib.geodesicline.GeodesicLine.salp1`
:attr:`~geographiclib.geodesicline.GeodesicLine.calp1`
:attr:`~geographiclib.geodesicline.GeodesicLine.s13`
:attr:`~geographiclib.geodesicline.GeodesicLine.a13`
"""
# geodesicline.py
#
# This is a rather literal translation of the GeographicLib::GeodesicLine class
# to python. See the documentation for the C++ class for more information at
#
# https://geographiclib.sourceforge.io/html/annotated.html
#
# The algorithms are derived in
#
# Charles F. F. Karney,
# Algorithms for geodesics, J. Geodesy 87, 43-55 (2013),
# https://doi.org/10.1007/s00190-012-0578-z
# Addenda: https://geographiclib.sourceforge.io/geod-addenda.html
#
# Copyright (c) Charles Karney (2011-2022) <charles@karney.com> and licensed
# under the MIT/X11 License. For more information, see
# https://geographiclib.sourceforge.io/
######################################################################
import math
from geographiclib.geomath import Math
from geographiclib.geodesiccapability import GeodesicCapability
[docs]class GeodesicLine:
"""Points on a geodesic path"""
def __init__(self, geod, lat1, lon1, azi1,
caps = GeodesicCapability.STANDARD |
GeodesicCapability.DISTANCE_IN,
salp1 = math.nan, calp1 = math.nan):
"""Construct a GeodesicLine object
:param geod: a :class:`~geographiclib.geodesic.Geodesic` object
:param lat1: latitude of the first point in degrees
:param lon1: longitude of the first point in degrees
:param azi1: azimuth at the first point in degrees
:param caps: the :ref:`capabilities <outmask>`
This creates an object allowing points along a geodesic starting at
(*lat1*, *lon1*), with azimuth *azi1* to be found. The default
value of *caps* is STANDARD | DISTANCE_IN. The optional parameters
*salp1* and *calp1* should not be supplied; they are part of the
private interface.
"""
from geographiclib.geodesic import Geodesic
self.a = geod.a
"""The equatorial radius in meters (readonly)"""
self.f = geod.f
"""The flattening (readonly)"""
self._b = geod._b
self._c2 = geod._c2
self._f1 = geod._f1
self.caps = (caps | Geodesic.LATITUDE | Geodesic.AZIMUTH |
Geodesic.LONG_UNROLL)
"""the capabilities (readonly)"""
# Guard against underflow in salp0
self.lat1 = Math.LatFix(lat1)
"""the latitude of the first point in degrees (readonly)"""
self.lon1 = lon1
"""the longitude of the first point in degrees (readonly)"""
if math.isnan(salp1) or math.isnan(calp1):
self.azi1 = Math.AngNormalize(azi1)
self.salp1, self.calp1 = Math.sincosd(Math.AngRound(azi1))
else:
self.azi1 = azi1
"""the azimuth at the first point in degrees (readonly)"""
self.salp1 = salp1
"""the sine of the azimuth at the first point (readonly)"""
self.calp1 = calp1
"""the cosine of the azimuth at the first point (readonly)"""
# real cbet1, sbet1
sbet1, cbet1 = Math.sincosd(Math.AngRound(self.lat1)); sbet1 *= self._f1
# Ensure cbet1 = +epsilon at poles
sbet1, cbet1 = Math.norm(sbet1, cbet1); cbet1 = max(Geodesic.tiny_, cbet1)
self._dn1 = math.sqrt(1 + geod._ep2 * Math.sq(sbet1))
# Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0),
self._salp0 = self.salp1 * cbet1 # alp0 in [0, pi/2 - |bet1|]
# Alt: calp0 = hypot(sbet1, calp1 * cbet1). The following
# is slightly better (consider the case salp1 = 0).
self._calp0 = math.hypot(self.calp1, self.salp1 * sbet1)
# Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1).
# sig = 0 is nearest northward crossing of equator.
# With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line).
# With bet1 = pi/2, alp1 = -pi, sig1 = pi/2
# With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2
# Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1).
# With alp0 in (0, pi/2], quadrants for sig and omg coincide.
# No atan2(0,0) ambiguity at poles since cbet1 = +epsilon.
# With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi.
self._ssig1 = sbet1; self._somg1 = self._salp0 * sbet1
self._csig1 = self._comg1 = (cbet1 * self.calp1
if sbet1 != 0 or self.calp1 != 0 else 1)
# sig1 in (-pi, pi]
self._ssig1, self._csig1 = Math.norm(self._ssig1, self._csig1)
# No need to normalize
# self._somg1, self._comg1 = Math.norm(self._somg1, self._comg1)
self._k2 = Math.sq(self._calp0) * geod._ep2
eps = self._k2 / (2 * (1 + math.sqrt(1 + self._k2)) + self._k2)
if self.caps & Geodesic.CAP_C1:
self._A1m1 = Geodesic._A1m1f(eps)
self._C1a = list(range(Geodesic.nC1_ + 1))
Geodesic._C1f(eps, self._C1a)
self._B11 = Geodesic._SinCosSeries(
True, self._ssig1, self._csig1, self._C1a)
s = math.sin(self._B11); c = math.cos(self._B11)
# tau1 = sig1 + B11
self._stau1 = self._ssig1 * c + self._csig1 * s
self._ctau1 = self._csig1 * c - self._ssig1 * s
# Not necessary because C1pa reverts C1a
# _B11 = -_SinCosSeries(true, _stau1, _ctau1, _C1pa)
if self.caps & Geodesic.CAP_C1p:
self._C1pa = list(range(Geodesic.nC1p_ + 1))
Geodesic._C1pf(eps, self._C1pa)
if self.caps & Geodesic.CAP_C2:
self._A2m1 = Geodesic._A2m1f(eps)
self._C2a = list(range(Geodesic.nC2_ + 1))
Geodesic._C2f(eps, self._C2a)
self._B21 = Geodesic._SinCosSeries(
True, self._ssig1, self._csig1, self._C2a)
if self.caps & Geodesic.CAP_C3:
self._C3a = list(range(Geodesic.nC3_))
geod._C3f(eps, self._C3a)
self._A3c = -self.f * self._salp0 * geod._A3f(eps)
self._B31 = Geodesic._SinCosSeries(
True, self._ssig1, self._csig1, self._C3a)
if self.caps & Geodesic.CAP_C4:
self._C4a = list(range(Geodesic.nC4_))
geod._C4f(eps, self._C4a)
# Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0)
self._A4 = Math.sq(self.a) * self._calp0 * self._salp0 * geod._e2
self._B41 = Geodesic._SinCosSeries(
False, self._ssig1, self._csig1, self._C4a)
self.s13 = math.nan
"""the distance between point 1 and point 3 in meters (readonly)"""
self.a13 = math.nan
"""the arc length between point 1 and point 3 in degrees (readonly)"""
# return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12
def _GenPosition(self, arcmode, s12_a12, outmask):
"""Private: General solution of position along geodesic"""
from geographiclib.geodesic import Geodesic
a12 = lat2 = lon2 = azi2 = s12 = m12 = M12 = M21 = S12 = math.nan
outmask &= self.caps & Geodesic.OUT_MASK
if not (arcmode or
(self.caps & (Geodesic.OUT_MASK & Geodesic.DISTANCE_IN))):
# Uninitialized or impossible distance calculation requested
return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12
# Avoid warning about uninitialized B12.
B12 = 0.0; AB1 = 0.0
if arcmode:
# Interpret s12_a12 as spherical arc length
sig12 = math.radians(s12_a12)
ssig12, csig12 = Math.sincosd(s12_a12)
else:
# Interpret s12_a12 as distance
tau12 = s12_a12 / (self._b * (1 + self._A1m1))
tau12 = tau12 if math.isfinite(tau12) else math.nan
s = math.sin(tau12); c = math.cos(tau12)
# tau2 = tau1 + tau12
B12 = - Geodesic._SinCosSeries(True,
self._stau1 * c + self._ctau1 * s,
self._ctau1 * c - self._stau1 * s,
self._C1pa)
sig12 = tau12 - (B12 - self._B11)
ssig12 = math.sin(sig12); csig12 = math.cos(sig12)
if abs(self.f) > 0.01:
# Reverted distance series is inaccurate for |f| > 1/100, so correct
# sig12 with 1 Newton iteration. The following table shows the
# approximate maximum error for a = WGS_a() and various f relative to
# GeodesicExact.
# erri = the error in the inverse solution (nm)
# errd = the error in the direct solution (series only) (nm)
# errda = the error in the direct solution (series + 1 Newton) (nm)
#
# f erri errd errda
# -1/5 12e6 1.2e9 69e6
# -1/10 123e3 12e6 765e3
# -1/20 1110 108e3 7155
# -1/50 18.63 200.9 27.12
# -1/100 18.63 23.78 23.37
# -1/150 18.63 21.05 20.26
# 1/150 22.35 24.73 25.83
# 1/100 22.35 25.03 25.31
# 1/50 29.80 231.9 30.44
# 1/20 5376 146e3 10e3
# 1/10 829e3 22e6 1.5e6
# 1/5 157e6 3.8e9 280e6
ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12
csig2 = self._csig1 * csig12 - self._ssig1 * ssig12
B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a)
serr = ((1 + self._A1m1) * (sig12 + (B12 - self._B11)) -
s12_a12 / self._b)
sig12 = sig12 - serr / math.sqrt(1 + self._k2 * Math.sq(ssig2))
ssig12 = math.sin(sig12); csig12 = math.cos(sig12)
# Update B12 below
# real omg12, lam12, lon12
# real ssig2, csig2, sbet2, cbet2, somg2, comg2, salp2, calp2
# sig2 = sig1 + sig12
ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12
csig2 = self._csig1 * csig12 - self._ssig1 * ssig12
dn2 = math.sqrt(1 + self._k2 * Math.sq(ssig2))
if outmask & (
Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE):
if arcmode or abs(self.f) > 0.01:
B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a)
AB1 = (1 + self._A1m1) * (B12 - self._B11)
# sin(bet2) = cos(alp0) * sin(sig2)
sbet2 = self._calp0 * ssig2
# Alt: cbet2 = hypot(csig2, salp0 * ssig2)
cbet2 = math.hypot(self._salp0, self._calp0 * csig2)
if cbet2 == 0:
# I.e., salp0 = 0, csig2 = 0. Break the degeneracy in this case
cbet2 = csig2 = Geodesic.tiny_
# tan(alp0) = cos(sig2)*tan(alp2)
salp2 = self._salp0; calp2 = self._calp0 * csig2 # No need to normalize
if outmask & Geodesic.DISTANCE:
s12 = self._b * ((1 + self._A1m1) * sig12 + AB1) if arcmode else s12_a12
if outmask & Geodesic.LONGITUDE:
# tan(omg2) = sin(alp0) * tan(sig2)
somg2 = self._salp0 * ssig2; comg2 = csig2 # No need to normalize
E = math.copysign(1, self._salp0) # East or west going?
# omg12 = omg2 - omg1
omg12 = (E * (sig12
- (math.atan2( ssig2, csig2) -
math.atan2( self._ssig1, self._csig1))
+ (math.atan2(E * somg2, comg2) -
math.atan2(E * self._somg1, self._comg1)))
if outmask & Geodesic.LONG_UNROLL
else math.atan2(somg2 * self._comg1 - comg2 * self._somg1,
comg2 * self._comg1 + somg2 * self._somg1))
lam12 = omg12 + self._A3c * (
sig12 + (Geodesic._SinCosSeries(True, ssig2, csig2, self._C3a)
- self._B31))
lon12 = math.degrees(lam12)
lon2 = (self.lon1 + lon12 if outmask & Geodesic.LONG_UNROLL else
Math.AngNormalize(Math.AngNormalize(self.lon1) +
Math.AngNormalize(lon12)))
if outmask & Geodesic.LATITUDE:
lat2 = Math.atan2d(sbet2, self._f1 * cbet2)
if outmask & Geodesic.AZIMUTH:
azi2 = Math.atan2d(salp2, calp2)
if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE):
B22 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C2a)
AB2 = (1 + self._A2m1) * (B22 - self._B21)
J12 = (self._A1m1 - self._A2m1) * sig12 + (AB1 - AB2)
if outmask & Geodesic.REDUCEDLENGTH:
# Add parens around (_csig1 * ssig2) and (_ssig1 * csig2) to ensure
# accurate cancellation in the case of coincident points.
m12 = self._b * (( dn2 * (self._csig1 * ssig2) -
self._dn1 * (self._ssig1 * csig2))
- self._csig1 * csig2 * J12)
if outmask & Geodesic.GEODESICSCALE:
t = (self._k2 * (ssig2 - self._ssig1) *
(ssig2 + self._ssig1) / (self._dn1 + dn2))
M12 = csig12 + (t * ssig2 - csig2 * J12) * self._ssig1 / self._dn1
M21 = csig12 - (t * self._ssig1 - self._csig1 * J12) * ssig2 / dn2
if outmask & Geodesic.AREA:
B42 = Geodesic._SinCosSeries(False, ssig2, csig2, self._C4a)
# real salp12, calp12
if self._calp0 == 0 or self._salp0 == 0:
# alp12 = alp2 - alp1, used in atan2 so no need to normalize
salp12 = salp2 * self.calp1 - calp2 * self.salp1
calp12 = calp2 * self.calp1 + salp2 * self.salp1
else:
# tan(alp) = tan(alp0) * sec(sig)
# tan(alp2-alp1) = (tan(alp2) -tan(alp1)) / (tan(alp2)*tan(alp1)+1)
# = calp0 * salp0 * (csig1-csig2) / (salp0^2 + calp0^2 * csig1*csig2)
# If csig12 > 0, write
# csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1)
# else
# csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1
# No need to normalize
salp12 = self._calp0 * self._salp0 * (
self._csig1 * (1 - csig12) + ssig12 * self._ssig1 if csig12 <= 0
else ssig12 * (self._csig1 * ssig12 / (1 + csig12) + self._ssig1))
calp12 = (Math.sq(self._salp0) +
Math.sq(self._calp0) * self._csig1 * csig2)
S12 = (self._c2 * math.atan2(salp12, calp12) +
self._A4 * (B42 - self._B41))
a12 = s12_a12 if arcmode else math.degrees(sig12)
return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12
[docs] def Position(self, s12, outmask = GeodesicCapability.STANDARD):
"""Find the position on the line given *s12*
:param s12: the distance from the first point to the second in
meters
:param outmask: the :ref:`output mask <outmask>`
:return: a :ref:`dict`
The default value of *outmask* is STANDARD, i.e., the *lat1*,
*lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are
returned. The :class:`~geographiclib.geodesicline.GeodesicLine`
object must have been constructed with the DISTANCE_IN capability.
"""
from geographiclib.geodesic import Geodesic
result = {'lat1': self.lat1,
'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else
Math.AngNormalize(self.lon1),
'azi1': self.azi1, 's12': s12}
a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition(
False, s12, outmask)
outmask &= Geodesic.OUT_MASK
result['a12'] = a12
if outmask & Geodesic.LATITUDE: result['lat2'] = lat2
if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2
if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2
if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12
if outmask & Geodesic.GEODESICSCALE:
result['M12'] = M12; result['M21'] = M21
if outmask & Geodesic.AREA: result['S12'] = S12
return result
[docs] def ArcPosition(self, a12, outmask = GeodesicCapability.STANDARD):
"""Find the position on the line given *a12*
:param a12: spherical arc length from the first point to the second
in degrees
:param outmask: the :ref:`output mask <outmask>`
:return: a :ref:`dict`
The default value of *outmask* is STANDARD, i.e., the *lat1*,
*lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are
returned.
"""
from geographiclib.geodesic import Geodesic
result = {'lat1': self.lat1,
'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else
Math.AngNormalize(self.lon1),
'azi1': self.azi1, 'a12': a12}
a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition(
True, a12, outmask)
outmask &= Geodesic.OUT_MASK
if outmask & Geodesic.DISTANCE: result['s12'] = s12
if outmask & Geodesic.LATITUDE: result['lat2'] = lat2
if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2
if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2
if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12
if outmask & Geodesic.GEODESICSCALE:
result['M12'] = M12; result['M21'] = M21
if outmask & Geodesic.AREA: result['S12'] = S12
return result
[docs] def SetDistance(self, s13):
"""Specify the position of point 3 in terms of distance
:param s13: distance from point 1 to point 3 in meters
"""
self.s13 = s13
self.a13, _, _, _, _, _, _, _, _ = self._GenPosition(False, self.s13, 0)
[docs] def SetArc(self, a13):
"""Specify the position of point 3 in terms of arc length
:param a13: spherical arc length from point 1 to point 3 in degrees
"""
from geographiclib.geodesic import Geodesic
self.a13 = a13
_, _, _, _, self.s13, _, _, _, _ = self._GenPosition(True, self.a13,
Geodesic.DISTANCE)