geodesicline.py
Go to the documentation of this file.
1 """Define the :class:`~geographiclib.geodesicline.GeodesicLine` class
2 
3 The constructor defines the starting point of the line. Points on the
4 line are given by
5 
6  * :meth:`~geographiclib.geodesicline.GeodesicLine.Position` position
7  given in terms of distance
8  * :meth:`~geographiclib.geodesicline.GeodesicLine.ArcPosition` position
9  given in terms of spherical arc length
10 
11 A reference point 3 can be defined with
12 
13  * :meth:`~geographiclib.geodesicline.GeodesicLine.SetDistance` set
14  position of 3 in terms of the distance from the starting point
15  * :meth:`~geographiclib.geodesicline.GeodesicLine.SetArc` set
16  position of 3 in terms of the spherical arc length from the starting point
17 
18 The object can also be constructed by
19 
20  * :meth:`Geodesic.Line <geographiclib.geodesic.Geodesic.Line>`
21  * :meth:`Geodesic.DirectLine <geographiclib.geodesic.Geodesic.DirectLine>`
22  * :meth:`Geodesic.ArcDirectLine
23  <geographiclib.geodesic.Geodesic.ArcDirectLine>`
24  * :meth:`Geodesic.InverseLine <geographiclib.geodesic.Geodesic.InverseLine>`
25 
26 The public attributes for this class are
27 
28  * :attr:`~geographiclib.geodesicline.GeodesicLine.a`
29  :attr:`~geographiclib.geodesicline.GeodesicLine.f`
30  :attr:`~geographiclib.geodesicline.GeodesicLine.caps`
31  :attr:`~geographiclib.geodesicline.GeodesicLine.lat1`
32  :attr:`~geographiclib.geodesicline.GeodesicLine.lon1`
33  :attr:`~geographiclib.geodesicline.GeodesicLine.azi1`
34  :attr:`~geographiclib.geodesicline.GeodesicLine.salp1`
35  :attr:`~geographiclib.geodesicline.GeodesicLine.calp1`
36  :attr:`~geographiclib.geodesicline.GeodesicLine.s13`
37  :attr:`~geographiclib.geodesicline.GeodesicLine.a13`
38 
39 """
40 # geodesicline.py
41 #
42 # This is a rather literal translation of the GeographicLib::GeodesicLine class
43 # to python. See the documentation for the C++ class for more information at
44 #
45 # https://geographiclib.sourceforge.io/html/annotated.html
46 #
47 # The algorithms are derived in
48 #
49 # Charles F. F. Karney,
50 # Algorithms for geodesics, J. Geodesy 87, 43-55 (2013),
51 # https://doi.org/10.1007/s00190-012-0578-z
52 # Addenda: https://geographiclib.sourceforge.io/geod-addenda.html
53 #
54 # Copyright (c) Charles Karney (2011-2016) <charles@karney.com> and licensed
55 # under the MIT/X11 License. For more information, see
56 # https://geographiclib.sourceforge.io/
57 ######################################################################
58 
59 import math
60 from geographiclib.geomath import Math
61 from geographiclib.geodesiccapability import GeodesicCapability
62 
64  """Points on a geodesic path"""
65 
66  def __init__(self, geod, lat1, lon1, azi1,
67  caps = GeodesicCapability.STANDARD |
68  GeodesicCapability.DISTANCE_IN,
69  salp1 = Math.nan, calp1 = Math.nan):
70  """Construct a GeodesicLine object
71 
72  :param geod: a :class:`~geographiclib.geodesic.Geodesic` object
73  :param lat1: latitude of the first point in degrees
74  :param lon1: longitude of the first point in degrees
75  :param azi1: azimuth at the first point in degrees
76  :param caps: the :ref:`capabilities <outmask>`
77 
78  This creates an object allowing points along a geodesic starting at
79  (*lat1*, *lon1*), with azimuth *azi1* to be found. The default
80  value of *caps* is STANDARD | DISTANCE_IN. The optional parameters
81  *salp1* and *calp1* should not be supplied; they are part of the
82  private interface.
83 
84  """
85 
86  from geographiclib.geodesic import Geodesic
87  self.a = geod.a
88  """The equatorial radius in meters (readonly)"""
89  self.f = geod.f
90  """The flattening (readonly)"""
91  self._b = geod._b
92  self._c2 = geod._c2
93  self._f1 = geod._f1
94  self.caps = (caps | Geodesic.LATITUDE | Geodesic.AZIMUTH |
95  Geodesic.LONG_UNROLL)
96  """the capabilities (readonly)"""
97 
98  # Guard against underflow in salp0
99  self.lat1 = Math.LatFix(lat1)
100  """the latitude of the first point in degrees (readonly)"""
101  self.lon1 = lon1
102  """the longitude of the first point in degrees (readonly)"""
103  if Math.isnan(salp1) or Math.isnan(calp1):
104  self.azi1 = Math.AngNormalize(azi1)
105  self.salp1, self.calp1 = Math.sincosd(Math.AngRound(azi1))
106  else:
107  self.azi1 = azi1
108  """the azimuth at the first point in degrees (readonly)"""
109  self.salp1 = salp1
110  """the sine of the azimuth at the first point (readonly)"""
111  self.calp1 = calp1
112  """the cosine of the azimuth at the first point (readonly)"""
113 
114  # real cbet1, sbet1
115  sbet1, cbet1 = Math.sincosd(Math.AngRound(lat1)); sbet1 *= self._f1
116  # Ensure cbet1 = +epsilon at poles
117  sbet1, cbet1 = Math.norm(sbet1, cbet1); cbet1 = max(Geodesic.tiny_, cbet1)
118  self._dn1 = math.sqrt(1 + geod._ep2 * Math.sq(sbet1))
119 
120  # Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0),
121  self._salp0 = self.salp1 * cbet1 # alp0 in [0, pi/2 - |bet1|]
122  # Alt: calp0 = hypot(sbet1, calp1 * cbet1). The following
123  # is slightly better (consider the case salp1 = 0).
124  self._calp0 = math.hypot(self.calp1, self.salp1 * sbet1)
125  # Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1).
126  # sig = 0 is nearest northward crossing of equator.
127  # With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line).
128  # With bet1 = pi/2, alp1 = -pi, sig1 = pi/2
129  # With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2
130  # Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1).
131  # With alp0 in (0, pi/2], quadrants for sig and omg coincide.
132  # No atan2(0,0) ambiguity at poles since cbet1 = +epsilon.
133  # With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi.
134  self._ssig1 = sbet1; self._somg1 = self._salp0 * sbet1
135  self._csig1 = self._comg1 = (cbet1 * self.calp1
136  if sbet1 != 0 or self.calp1 != 0 else 1)
137  # sig1 in (-pi, pi]
138  self._ssig1, self._csig1 = Math.norm(self._ssig1, self._csig1)
139  # No need to normalize
140  # self._somg1, self._comg1 = Math.norm(self._somg1, self._comg1)
141 
142  self._k2 = Math.sq(self._calp0) * geod._ep2
143  eps = self._k2 / (2 * (1 + math.sqrt(1 + self._k2)) + self._k2)
144 
145  if self.caps & Geodesic.CAP_C1:
146  self._A1m1 = Geodesic._A1m1f(eps)
147  self._C1a = list(range(Geodesic.nC1_ + 1))
148  Geodesic._C1f(eps, self._C1a)
149  self._B11 = Geodesic._SinCosSeries(
150  True, self._ssig1, self._csig1, self._C1a)
151  s = math.sin(self._B11); c = math.cos(self._B11)
152  # tau1 = sig1 + B11
153  self._stau1 = self._ssig1 * c + self._csig1 * s
154  self._ctau1 = self._csig1 * c - self._ssig1 * s
155  # Not necessary because C1pa reverts C1a
156  # _B11 = -_SinCosSeries(true, _stau1, _ctau1, _C1pa)
157 
158  if self.caps & Geodesic.CAP_C1p:
159  self._C1pa = list(range(Geodesic.nC1p_ + 1))
160  Geodesic._C1pf(eps, self._C1pa)
161 
162  if self.caps & Geodesic.CAP_C2:
163  self._A2m1 = Geodesic._A2m1f(eps)
164  self._C2a = list(range(Geodesic.nC2_ + 1))
165  Geodesic._C2f(eps, self._C2a)
166  self._B21 = Geodesic._SinCosSeries(
167  True, self._ssig1, self._csig1, self._C2a)
168 
169  if self.caps & Geodesic.CAP_C3:
170  self._C3a = list(range(Geodesic.nC3_))
171  geod._C3f(eps, self._C3a)
172  self._A3c = -self.f * self._salp0 * geod._A3f(eps)
173  self._B31 = Geodesic._SinCosSeries(
174  True, self._ssig1, self._csig1, self._C3a)
175 
176  if self.caps & Geodesic.CAP_C4:
177  self._C4a = list(range(Geodesic.nC4_))
178  geod._C4f(eps, self._C4a)
179  # Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0)
180  self._A4 = Math.sq(self.a) * self._calp0 * self._salp0 * geod._e2
181  self._B41 = Geodesic._SinCosSeries(
182  False, self._ssig1, self._csig1, self._C4a)
183  self.s13 = Math.nan
184  """the distance between point 1 and point 3 in meters (readonly)"""
185  self.a13 = Math.nan
186  """the arc length between point 1 and point 3 in degrees (readonly)"""
187 
188  # return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12
189  def _GenPosition(self, arcmode, s12_a12, outmask):
190  """Private: General solution of position along geodesic"""
191  from geographiclib.geodesic import Geodesic
192  a12 = lat2 = lon2 = azi2 = s12 = m12 = M12 = M21 = S12 = Math.nan
193  outmask &= self.caps & Geodesic.OUT_MASK
194  if not (arcmode or
195  (self.caps & (Geodesic.OUT_MASK & Geodesic.DISTANCE_IN))):
196  # Uninitialized or impossible distance calculation requested
197  return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12
198 
199  # Avoid warning about uninitialized B12.
200  B12 = 0.0; AB1 = 0.0
201  if arcmode:
202  # Interpret s12_a12 as spherical arc length
203  sig12 = math.radians(s12_a12)
204  ssig12, csig12 = Math.sincosd(s12_a12)
205  else:
206  # Interpret s12_a12 as distance
207  tau12 = s12_a12 / (self._b * (1 + self._A1m1))
208  s = math.sin(tau12); c = math.cos(tau12)
209  # tau2 = tau1 + tau12
210  B12 = - Geodesic._SinCosSeries(True,
211  self._stau1 * c + self._ctau1 * s,
212  self._ctau1 * c - self._stau1 * s,
213  self._C1pa)
214  sig12 = tau12 - (B12 - self._B11)
215  ssig12 = math.sin(sig12); csig12 = math.cos(sig12)
216  if abs(self.f) > 0.01:
217  # Reverted distance series is inaccurate for |f| > 1/100, so correct
218  # sig12 with 1 Newton iteration. The following table shows the
219  # approximate maximum error for a = WGS_a() and various f relative to
220  # GeodesicExact.
221  # erri = the error in the inverse solution (nm)
222  # errd = the error in the direct solution (series only) (nm)
223  # errda = the error in the direct solution (series + 1 Newton) (nm)
224  #
225  # f erri errd errda
226  # -1/5 12e6 1.2e9 69e6
227  # -1/10 123e3 12e6 765e3
228  # -1/20 1110 108e3 7155
229  # -1/50 18.63 200.9 27.12
230  # -1/100 18.63 23.78 23.37
231  # -1/150 18.63 21.05 20.26
232  # 1/150 22.35 24.73 25.83
233  # 1/100 22.35 25.03 25.31
234  # 1/50 29.80 231.9 30.44
235  # 1/20 5376 146e3 10e3
236  # 1/10 829e3 22e6 1.5e6
237  # 1/5 157e6 3.8e9 280e6
238  ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12
239  csig2 = self._csig1 * csig12 - self._ssig1 * ssig12
240  B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a)
241  serr = ((1 + self._A1m1) * (sig12 + (B12 - self._B11)) -
242  s12_a12 / self._b)
243  sig12 = sig12 - serr / math.sqrt(1 + self._k2 * Math.sq(ssig2))
244  ssig12 = math.sin(sig12); csig12 = math.cos(sig12)
245  # Update B12 below
246 
247  # real omg12, lam12, lon12
248  # real ssig2, csig2, sbet2, cbet2, somg2, comg2, salp2, calp2
249  # sig2 = sig1 + sig12
250  ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12
251  csig2 = self._csig1 * csig12 - self._ssig1 * ssig12
252  dn2 = math.sqrt(1 + self._k2 * Math.sq(ssig2))
253  if outmask & (
254  Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE):
255  if arcmode or abs(self.f) > 0.01:
256  B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a)
257  AB1 = (1 + self._A1m1) * (B12 - self._B11)
258  # sin(bet2) = cos(alp0) * sin(sig2)
259  sbet2 = self._calp0 * ssig2
260  # Alt: cbet2 = hypot(csig2, salp0 * ssig2)
261  cbet2 = math.hypot(self._salp0, self._calp0 * csig2)
262  if cbet2 == 0:
263  # I.e., salp0 = 0, csig2 = 0. Break the degeneracy in this case
264  cbet2 = csig2 = Geodesic.tiny_
265  # tan(alp0) = cos(sig2)*tan(alp2)
266  salp2 = self._salp0; calp2 = self._calp0 * csig2 # No need to normalize
267 
268  if outmask & Geodesic.DISTANCE:
269  s12 = self._b * ((1 + self._A1m1) * sig12 + AB1) if arcmode else s12_a12
270 
271  if outmask & Geodesic.LONGITUDE:
272  # tan(omg2) = sin(alp0) * tan(sig2)
273  somg2 = self._salp0 * ssig2; comg2 = csig2 # No need to normalize
274  E = Math.copysign(1, self._salp0) # East or west going?
275  # omg12 = omg2 - omg1
276  omg12 = (E * (sig12
277  - (math.atan2( ssig2, csig2) -
278  math.atan2( self._ssig1, self._csig1))
279  + (math.atan2(E * somg2, comg2) -
280  math.atan2(E * self._somg1, self._comg1)))
281  if outmask & Geodesic.LONG_UNROLL
282  else math.atan2(somg2 * self._comg1 - comg2 * self._somg1,
283  comg2 * self._comg1 + somg2 * self._somg1))
284  lam12 = omg12 + self._A3c * (
285  sig12 + (Geodesic._SinCosSeries(True, ssig2, csig2, self._C3a)
286  - self._B31))
287  lon12 = math.degrees(lam12)
288  lon2 = (self.lon1 + lon12 if outmask & Geodesic.LONG_UNROLL else
289  Math.AngNormalize(Math.AngNormalize(self.lon1) +
290  Math.AngNormalize(lon12)))
291 
292  if outmask & Geodesic.LATITUDE:
293  lat2 = Math.atan2d(sbet2, self._f1 * cbet2)
294 
295  if outmask & Geodesic.AZIMUTH:
296  azi2 = Math.atan2d(salp2, calp2)
297 
298  if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE):
299  B22 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C2a)
300  AB2 = (1 + self._A2m1) * (B22 - self._B21)
301  J12 = (self._A1m1 - self._A2m1) * sig12 + (AB1 - AB2)
302  if outmask & Geodesic.REDUCEDLENGTH:
303  # Add parens around (_csig1 * ssig2) and (_ssig1 * csig2) to ensure
304  # accurate cancellation in the case of coincident points.
305  m12 = self._b * (( dn2 * (self._csig1 * ssig2) -
306  self._dn1 * (self._ssig1 * csig2))
307  - self._csig1 * csig2 * J12)
308  if outmask & Geodesic.GEODESICSCALE:
309  t = (self._k2 * (ssig2 - self._ssig1) *
310  (ssig2 + self._ssig1) / (self._dn1 + dn2))
311  M12 = csig12 + (t * ssig2 - csig2 * J12) * self._ssig1 / self._dn1
312  M21 = csig12 - (t * self._ssig1 - self._csig1 * J12) * ssig2 / dn2
313 
314  if outmask & Geodesic.AREA:
315  B42 = Geodesic._SinCosSeries(False, ssig2, csig2, self._C4a)
316  # real salp12, calp12
317  if self._calp0 == 0 or self._salp0 == 0:
318  # alp12 = alp2 - alp1, used in atan2 so no need to normalize
319  salp12 = salp2 * self.calp1 - calp2 * self.salp1
320  calp12 = calp2 * self.calp1 + salp2 * self.salp1
321  else:
322  # tan(alp) = tan(alp0) * sec(sig)
323  # tan(alp2-alp1) = (tan(alp2) -tan(alp1)) / (tan(alp2)*tan(alp1)+1)
324  # = calp0 * salp0 * (csig1-csig2) / (salp0^2 + calp0^2 * csig1*csig2)
325  # If csig12 > 0, write
326  # csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1)
327  # else
328  # csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1
329  # No need to normalize
330  salp12 = self._calp0 * self._salp0 * (
331  self._csig1 * (1 - csig12) + ssig12 * self._ssig1 if csig12 <= 0
332  else ssig12 * (self._csig1 * ssig12 / (1 + csig12) + self._ssig1))
333  calp12 = (Math.sq(self._salp0) +
334  Math.sq(self._calp0) * self._csig1 * csig2)
335  S12 = (self._c2 * math.atan2(salp12, calp12) +
336  self._A4 * (B42 - self._B41))
337 
338  a12 = s12_a12 if arcmode else math.degrees(sig12)
339  return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12
340 
341  def Position(self, s12, outmask = GeodesicCapability.STANDARD):
342  """Find the position on the line given *s12*
343 
344  :param s12: the distance from the first point to the second in
345  meters
346  :param outmask: the :ref:`output mask <outmask>`
347  :return: a :ref:`dict`
348 
349  The default value of *outmask* is STANDARD, i.e., the *lat1*,
350  *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are
351  returned. The :class:`~geographiclib.geodesicline.GeodesicLine`
352  object must have been constructed with the DISTANCE_IN capability.
353 
354  """
355 
356  from geographiclib.geodesic import Geodesic
357  result = {'lat1': self.lat1,
358  'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else
359  Math.AngNormalize(self.lon1),
360  'azi1': self.azi1, 's12': s12}
361  a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition(
362  False, s12, outmask)
363  outmask &= Geodesic.OUT_MASK
364  result['a12'] = a12
365  if outmask & Geodesic.LATITUDE: result['lat2'] = lat2
366  if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2
367  if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2
368  if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12
369  if outmask & Geodesic.GEODESICSCALE:
370  result['M12'] = M12; result['M21'] = M21
371  if outmask & Geodesic.AREA: result['S12'] = S12
372  return result
373 
374  def ArcPosition(self, a12, outmask = GeodesicCapability.STANDARD):
375  """Find the position on the line given *a12*
376 
377  :param a12: spherical arc length from the first point to the second
378  in degrees
379  :param outmask: the :ref:`output mask <outmask>`
380  :return: a :ref:`dict`
381 
382  The default value of *outmask* is STANDARD, i.e., the *lat1*,
383  *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are
384  returned.
385 
386  """
387 
388  from geographiclib.geodesic import Geodesic
389  result = {'lat1': self.lat1,
390  'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else
391  Math.AngNormalize(self.lon1),
392  'azi1': self.azi1, 'a12': a12}
393  a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition(
394  True, a12, outmask)
395  outmask &= Geodesic.OUT_MASK
396  if outmask & Geodesic.DISTANCE: result['s12'] = s12
397  if outmask & Geodesic.LATITUDE: result['lat2'] = lat2
398  if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2
399  if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2
400  if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12
401  if outmask & Geodesic.GEODESICSCALE:
402  result['M12'] = M12; result['M21'] = M21
403  if outmask & Geodesic.AREA: result['S12'] = S12
404  return result
405 
406  def SetDistance(self, s13):
407  """Specify the position of point 3 in terms of distance
408 
409  :param s13: distance from point 1 to point 3 in meters
410 
411  """
412 
413  self.s13 = s13
414  self.a13, _, _, _, _, _, _, _, _ = self._GenPosition(False, self.s13, 0)
415 
416  def SetArc(self, a13):
417  """Specify the position of point 3 in terms of arc length
418 
419  :param a13: spherical arc length from point 1 to point 3 in degrees
420 
421  """
422 
423  from geographiclib.geodesic import Geodesic
424  self.a13 = a13
425  _, _, _, _, self.s13, _, _, _, _ = self._GenPosition(True, self.a13,
426  Geodesic.DISTANCE)
#define max(a, b)
Definition: datatypes.h:20
def Position(self, s12, outmask=GeodesicCapability.STANDARD)
def ArcPosition(self, a12, outmask=GeodesicCapability.STANDARD)
def _GenPosition(self, arcmode, s12_a12, outmask)
Definition: pytypes.h:1301
#define abs(x)
Definition: datatypes.h:17
def __init__(self, geod, lat1, lon1, azi1, caps=GeodesicCapability.STANDARD|GeodesicCapability.DISTANCE_IN, salp1=Math.nan, calp1=Math.nan)
Definition: geodesicline.py:69


gtsam
Author(s):
autogenerated on Sat May 8 2021 02:42:08