pygeodesy 24.3.24__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. PyGeodesy-24.3.24.dist-info/METADATA +272 -0
  2. PyGeodesy-24.3.24.dist-info/RECORD +115 -0
  3. PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
  4. PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
  5. pygeodesy/LICENSE +21 -0
  6. pygeodesy/__init__.py +615 -0
  7. pygeodesy/__main__.py +103 -0
  8. pygeodesy/albers.py +867 -0
  9. pygeodesy/auxilats/_CX_4.py +218 -0
  10. pygeodesy/auxilats/_CX_6.py +314 -0
  11. pygeodesy/auxilats/_CX_8.py +475 -0
  12. pygeodesy/auxilats/__init__.py +54 -0
  13. pygeodesy/auxilats/__main__.py +86 -0
  14. pygeodesy/auxilats/auxAngle.py +548 -0
  15. pygeodesy/auxilats/auxDLat.py +302 -0
  16. pygeodesy/auxilats/auxDST.py +296 -0
  17. pygeodesy/auxilats/auxLat.py +848 -0
  18. pygeodesy/auxilats/auxily.py +272 -0
  19. pygeodesy/azimuthal.py +1150 -0
  20. pygeodesy/basics.py +892 -0
  21. pygeodesy/booleans.py +2031 -0
  22. pygeodesy/cartesianBase.py +1062 -0
  23. pygeodesy/clipy.py +704 -0
  24. pygeodesy/constants.py +516 -0
  25. pygeodesy/css.py +660 -0
  26. pygeodesy/datums.py +752 -0
  27. pygeodesy/deprecated/__init__.py +61 -0
  28. pygeodesy/deprecated/bases.py +40 -0
  29. pygeodesy/deprecated/classes.py +262 -0
  30. pygeodesy/deprecated/consterns.py +54 -0
  31. pygeodesy/deprecated/datum.py +40 -0
  32. pygeodesy/deprecated/functions.py +375 -0
  33. pygeodesy/deprecated/nvector.py +48 -0
  34. pygeodesy/deprecated/rhumbBase.py +32 -0
  35. pygeodesy/deprecated/rhumbaux.py +33 -0
  36. pygeodesy/deprecated/rhumbsolve.py +33 -0
  37. pygeodesy/deprecated/rhumbx.py +33 -0
  38. pygeodesy/dms.py +986 -0
  39. pygeodesy/ecef.py +1348 -0
  40. pygeodesy/elevations.py +279 -0
  41. pygeodesy/ellipsoidalBase.py +1224 -0
  42. pygeodesy/ellipsoidalBaseDI.py +913 -0
  43. pygeodesy/ellipsoidalExact.py +343 -0
  44. pygeodesy/ellipsoidalGeodSolve.py +343 -0
  45. pygeodesy/ellipsoidalKarney.py +403 -0
  46. pygeodesy/ellipsoidalNvector.py +685 -0
  47. pygeodesy/ellipsoidalVincenty.py +590 -0
  48. pygeodesy/ellipsoids.py +2476 -0
  49. pygeodesy/elliptic.py +1198 -0
  50. pygeodesy/epsg.py +243 -0
  51. pygeodesy/errors.py +804 -0
  52. pygeodesy/etm.py +1190 -0
  53. pygeodesy/fmath.py +1013 -0
  54. pygeodesy/formy.py +1818 -0
  55. pygeodesy/frechet.py +865 -0
  56. pygeodesy/fstats.py +760 -0
  57. pygeodesy/fsums.py +1898 -0
  58. pygeodesy/gars.py +358 -0
  59. pygeodesy/geodesicw.py +581 -0
  60. pygeodesy/geodesicx/_C4_24.py +1699 -0
  61. pygeodesy/geodesicx/_C4_27.py +2395 -0
  62. pygeodesy/geodesicx/_C4_30.py +3301 -0
  63. pygeodesy/geodesicx/__init__.py +48 -0
  64. pygeodesy/geodesicx/__main__.py +91 -0
  65. pygeodesy/geodesicx/gx.py +1382 -0
  66. pygeodesy/geodesicx/gxarea.py +535 -0
  67. pygeodesy/geodesicx/gxbases.py +154 -0
  68. pygeodesy/geodesicx/gxline.py +669 -0
  69. pygeodesy/geodsolve.py +426 -0
  70. pygeodesy/geohash.py +914 -0
  71. pygeodesy/geoids.py +1884 -0
  72. pygeodesy/hausdorff.py +892 -0
  73. pygeodesy/heights.py +1155 -0
  74. pygeodesy/interns.py +687 -0
  75. pygeodesy/iters.py +545 -0
  76. pygeodesy/karney.py +919 -0
  77. pygeodesy/ktm.py +633 -0
  78. pygeodesy/latlonBase.py +1766 -0
  79. pygeodesy/lazily.py +960 -0
  80. pygeodesy/lcc.py +684 -0
  81. pygeodesy/ltp.py +1107 -0
  82. pygeodesy/ltpTuples.py +1563 -0
  83. pygeodesy/mgrs.py +721 -0
  84. pygeodesy/named.py +1324 -0
  85. pygeodesy/namedTuples.py +683 -0
  86. pygeodesy/nvectorBase.py +695 -0
  87. pygeodesy/osgr.py +781 -0
  88. pygeodesy/points.py +1686 -0
  89. pygeodesy/props.py +628 -0
  90. pygeodesy/resections.py +1048 -0
  91. pygeodesy/rhumb/__init__.py +46 -0
  92. pygeodesy/rhumb/aux_.py +397 -0
  93. pygeodesy/rhumb/bases.py +1148 -0
  94. pygeodesy/rhumb/ekx.py +563 -0
  95. pygeodesy/rhumb/solve.py +572 -0
  96. pygeodesy/simplify.py +647 -0
  97. pygeodesy/solveBase.py +472 -0
  98. pygeodesy/sphericalBase.py +724 -0
  99. pygeodesy/sphericalNvector.py +1264 -0
  100. pygeodesy/sphericalTrigonometry.py +1447 -0
  101. pygeodesy/streprs.py +627 -0
  102. pygeodesy/trf.py +2079 -0
  103. pygeodesy/triaxials.py +1484 -0
  104. pygeodesy/units.py +969 -0
  105. pygeodesy/unitsBase.py +349 -0
  106. pygeodesy/ups.py +538 -0
  107. pygeodesy/utily.py +1231 -0
  108. pygeodesy/utm.py +762 -0
  109. pygeodesy/utmups.py +318 -0
  110. pygeodesy/utmupsBase.py +517 -0
  111. pygeodesy/vector2d.py +785 -0
  112. pygeodesy/vector3d.py +968 -0
  113. pygeodesy/vector3dBase.py +1049 -0
  114. pygeodesy/webmercator.py +383 -0
  115. pygeodesy/wgrs.py +439 -0
@@ -0,0 +1,1382 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
4
+ u'''A pure Python version of I{Karney}'s C++ class U{GeodesicExact
5
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}.
6
+
7
+ Class L{GeodesicExact} follows the naming, methods and return values
8
+ of class C{Geodesic} from I{Karney}'s Python U{geographiclib
9
+ <https://GitHub.com/geographiclib/geographiclib-python>}.
10
+
11
+ Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2023)
12
+ and licensed under the MIT/X11 License. For more information, see the
13
+ U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.
14
+ '''
15
+ # make sure int/int division yields float quotient
16
+ from __future__ import division as _; del _ # PYCHOK semicolon
17
+
18
+ # A copy of comments from Karney's C{GeodesicExact.cpp}:
19
+ #
20
+ # This is a reformulation of the geodesic problem. The
21
+ # notation is as follows:
22
+ # - at a general point (no suffix or 1 or 2 as suffix)
23
+ # - phi = latitude
24
+ # - lambda = longitude
25
+ # - beta = latitude on auxiliary sphere
26
+ # - omega = longitude on auxiliary sphere
27
+ # - alpha = azimuth of great circle
28
+ # - sigma = arc length along great circle
29
+ # - s = distance
30
+ # - tau = scaled distance (= sigma at multiples of PI/2)
31
+ # - at northwards equator crossing
32
+ # - beta = phi = 0
33
+ # - omega = lambda = 0
34
+ # - alpha = alpha0
35
+ # - sigma = s = 0
36
+ # - a 12 suffix means a difference, e.g., s12 = s2 - s1.
37
+ # - s and c prefixes mean sin and cos
38
+
39
+ from pygeodesy.basics import _copysign, _xinstanceof, _xor, unsigned0
40
+ from pygeodesy.constants import EPS, EPS0, EPS02, MANT_DIG, NAN, PI, _EPSqrt, \
41
+ _SQRT2_2, isnan, _0_0, _0_001, _0_01, _0_1, _0_5, \
42
+ _1_0, _N_1_0, _1_75, _2_0, _N_2_0, _2__PI, _3_0, \
43
+ _4_0, _6_0, _8_0, _16_0, _90_0, _180_0, _1000_0
44
+ from pygeodesy.datums import _earth_datum, _WGS84, _EWGS84
45
+ # from pygeodesy.ellipsoids import _EWGS84 # from .datums
46
+ from pygeodesy.fmath import hypot as _hypot
47
+ from pygeodesy.fsums import fsumf_, fsum1f_
48
+ from pygeodesy.geodesicx.gxbases import _cosSeries, _GeodesicBase, \
49
+ _sincos12, _sin1cos2, _xnC4
50
+ from pygeodesy.geodesicx.gxline import _GeodesicLineExact, _TINY, _update_glXs
51
+ from pygeodesy.interns import NN, _COMMASPACE_, _DOT_, _UNDER_
52
+ from pygeodesy.karney import _around, _atan2d, Caps, _cbrt, _diff182, \
53
+ _fix90, GDict, GeodesicError, _K_2_0, \
54
+ _norm2, _norm180, _polynomial, _signBit, \
55
+ _sincos2, _sincos2d, _sincos2de, _unsigned2
56
+ from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS
57
+ from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple
58
+ from pygeodesy.props import deprecated_Property, Property, Property_RO, property_RO
59
+ from pygeodesy.streprs import Fmt, pairs
60
+ from pygeodesy.utily import atan2d as _atan2d_reverse, _unrollon, _Wrap, wrap360
61
+
62
+ from math import atan2, copysign, cos, degrees, fabs, radians, sqrt
63
+
64
+ __all__ = ()
65
+ __version__ = '24.02.21'
66
+
67
+ _MAXIT1 = 20
68
+ _MAXIT2 = 10 + _MAXIT1 + MANT_DIG # MANT_DIG == C++ digits
69
+
70
+ # increased multiplier in defn of _TOL1 from 100 to 200 to fix Inverse
71
+ # case 52.784459512564 0 -52.784459512563990912 179.634407464943777557
72
+ # which otherwise failed for Visual Studio 10 (Release and Debug)
73
+ _TOL0 = EPS
74
+ _TOL1 = _TOL0 * -200 # negative
75
+ _TOL2 = _EPSqrt # == sqrt(_TOL0)
76
+ _TOL3 = _TOL2 * _0_1
77
+ _TOLb = _TOL2 * _TOL0 # Check on bisection interval
78
+ _THR1 = _TOL2 * _1000_0 + _1_0
79
+
80
+ _TINY3 = _TINY * _3_0
81
+ _TOL08 = _TOL0 * _8_0
82
+ _TOL016 = _TOL0 * _16_0
83
+
84
+
85
+ def _atan12(*sincos12, **sineg0):
86
+ '''(INTERNAL) Return C{ang12} in C{radians}.
87
+ '''
88
+ return atan2(*_sincos12(*sincos12, **sineg0))
89
+
90
+
91
+ def _eTOL2(f):
92
+ # Using the auxiliary sphere solution with dnm computed at
93
+ # (bet1 + bet2) / 2, the relative error in the azimuth
94
+ # consistency check is sig12^2 * abs(f) * min(1, 1-f/2) / 2.
95
+ # (Error measured for 1/100 < b/a < 100 and abs(f) >= 1/1000.
96
+
97
+ # For a given f and sig12, the max error occurs for lines
98
+ # near the pole. If the old rule for computing dnm = (dn1
99
+ # + dn2)/2 is used, then the error increases by a factor of
100
+ # 2.) Setting this equal to epsilon gives sig12 = etol2.
101
+
102
+ # Here 0.1 is a safety factor (error decreased by 100) and
103
+ # max(0.001, abs(f)) stops etol2 getting too large in the
104
+ # nearly spherical case.
105
+ t = min(_1_0, _1_0 - f * _0_5) * max(_0_001, fabs(f)) * _0_5
106
+ return _TOL3 / (sqrt(t) if t > EPS02 else EPS0)
107
+
108
+
109
+ class _PDict(GDict):
110
+ '''(INTERNAL) Parameters passed around in C{._GDictInverse} and
111
+ optionally returned when C{GeodesicExact.debug} is C{True}.
112
+ '''
113
+ def set_sigs(self, ssig1, csig1, ssig2, csig2):
114
+ '''Update the C{sig1} and C{sig2} parameters.
115
+ '''
116
+ self.set_(ssig1=ssig1, csig1=csig1, sncndn1=(ssig1, csig1, self.dn1), # PYCHOK dn1
117
+ ssig2=ssig2, csig2=csig2, sncndn2=(ssig2, csig2, self.dn2)) # PYCHOK dn2
118
+
119
+ def toGDict(self): # PYCHOK no cover
120
+ '''Return as C{GDict} without attrs C{sncndn1} and C{sncndn2}.
121
+ '''
122
+ def _rest(sncndn1=None, sncndn2=None, **rest): # PYCHOK sncndn* not used
123
+ return GDict(rest)
124
+
125
+ return _rest(**self)
126
+
127
+
128
+ class GeodesicExact(_GeodesicBase):
129
+ '''A pure Python version of I{Karney}'s C++ class U{GeodesicExact
130
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>},
131
+ modeled after I{Karney}'s Python class U{geodesic.Geodesic<https://GitHub.com/
132
+ geographiclib/geographiclib-python>}.
133
+ '''
134
+ _datum = _WGS84
135
+ _nC4 = 30 # default C4order
136
+
137
+ def __init__(self, a_ellipsoid=_EWGS84, f=None, name=NN, C4order=None,
138
+ C4Order=None): # for backward compatibility
139
+ '''New L{GeodesicExact} instance.
140
+
141
+ @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{Datum}) or
142
+ the equatorial radius of the ellipsoid (C{scalar},
143
+ conventionally in C{meter}), see B{C{f}}.
144
+ @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}}
145
+ is specified as C{scalar}.
146
+ @kwarg name: Optional name (C{str}).
147
+ @kwarg C4order: Optional series expansion order (C{int}), see property
148
+ L{C4order}, default C{30}.
149
+ @kwarg C4Order: DEPRECATED, use keyword argument B{C{C4order}}.
150
+
151
+ @raise GeodesicError: Invalid B{C{C4order}}.
152
+ '''
153
+ _earth_datum(self, a_ellipsoid, f=f, name=name)
154
+ if name:
155
+ self.name = name
156
+ if C4order: # XXX private copy, always?
157
+ self.C4order = C4order
158
+ elif C4Order: # for backward compatibility
159
+ self.C4Order = C4Order
160
+
161
+ @Property_RO
162
+ def a(self):
163
+ '''Get the I{equatorial} radius, semi-axis (C{meter}).
164
+ '''
165
+ return self.ellipsoid.a
166
+
167
+ def ArcDirect(self, lat1, lon1, azi1, a12, outmask=Caps.STANDARD):
168
+ '''Solve the I{Direct} geodesic problem in terms of (spherical) arc length.
169
+
170
+ @arg lat1: Latitude of the first point (C{degrees}).
171
+ @arg lon1: Longitude of the first point (C{degrees}).
172
+ @arg azi1: Azimuth at the first point (compass C{degrees}).
173
+ @arg a12: Arc length between the points (C{degrees}), can be negative.
174
+ @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
175
+ the quantities to be returned.
176
+
177
+ @return: A L{GDict} with up to 12 items C{lat1, lon1, azi1, lat2,
178
+ lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},
179
+ C{lon1}, C{azi1} and arc length C{a12} always included.
180
+
181
+ @see: C++ U{GeodesicExact.ArcDirect
182
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}
183
+ and Python U{Geodesic.ArcDirect<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
184
+ '''
185
+ return self._GDictDirect(lat1, lon1, azi1, True, a12, outmask)
186
+
187
+ def ArcDirectLine(self, lat1, lon1, azi1, a12, caps=Caps.ALL, name=NN):
188
+ '''Define a L{GeodesicLineExact} in terms of the I{direct} geodesic problem and as arc length.
189
+
190
+ @arg lat1: Latitude of the first point (C{degrees}).
191
+ @arg lon1: Longitude of the first point (C{degrees}).
192
+ @arg azi1: Azimuth at the first point (compass C{degrees}).
193
+ @arg a12: Arc length between the points (C{degrees}), can be negative.
194
+ @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
195
+ the capabilities the L{GeodesicLineExact} instance
196
+ should possess, i.e., which quantities can be
197
+ returned by calls to L{GeodesicLineExact.Position}
198
+ and L{GeodesicLineExact.ArcPosition}.
199
+
200
+ @return: A L{GeodesicLineExact} instance.
201
+
202
+ @note: The third point of the L{GeodesicLineExact} is set to correspond
203
+ to the second point of the I{Inverse} geodesic problem.
204
+
205
+ @note: Latitude B{C{lat1}} should in the range C{[-90, +90]}.
206
+
207
+ @see: C++ U{GeodesicExact.ArcDirectLine
208
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and
209
+ Python U{Geodesic.ArcDirectLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
210
+ '''
211
+ return self._GenDirectLine(lat1, lon1, azi1, True, a12, caps, name=name)
212
+
213
+ def Area(self, polyline=False, name=NN):
214
+ '''Set up a L{GeodesicAreaExact} to compute area and
215
+ perimeter of a polygon.
216
+
217
+ @kwarg polyline: If C{True} perimeter only, otherwise
218
+ area and perimeter (C{bool}).
219
+ @kwarg name: Optional name (C{str}).
220
+
221
+ @return: A L{GeodesicAreaExact} instance.
222
+
223
+ @note: The B{C{debug}} setting is passed as C{verbose}
224
+ to the returned L{GeodesicAreaExact} instance.
225
+ '''
226
+ gaX = _MODS.geodesicx.GeodesicAreaExact(self, polyline=polyline,
227
+ name=name or self.name)
228
+ if self.debug:
229
+ gaX.verbose = True
230
+ return gaX
231
+
232
+ @Property_RO
233
+ def b(self):
234
+ '''Get the ellipsoid's I{polar} radius, semi-axis (C{meter}).
235
+ '''
236
+ return self.ellipsoid.b
237
+
238
+ @Property_RO
239
+ def c2x(self):
240
+ '''Get the ellipsoid's I{authalic} earth radius I{squared} (C{meter} I{squared}).
241
+ '''
242
+ # The Geodesic class substitutes atanh(sqrt(e2)) for asinh(sqrt(ep2))
243
+ # in the definition of _c2. The latter is more accurate for very
244
+ # oblate ellipsoids (which the Geodesic class does not handle). Of
245
+ # course, the area calculation in GeodesicExact is still based on a
246
+ # series and only holds for moderately oblate (or prolate) ellipsoids.
247
+ return self.ellipsoid.c2x
248
+
249
+ c2 = c2x # in this particular case
250
+
251
+ def C4f(self, eps):
252
+ '''Evaluate the C{C4x} coefficients for B{C{eps}}.
253
+
254
+ @arg eps: Polynomial factor (C{float}).
255
+
256
+ @return: C{C4order}-Tuple of C{C4x(B{eps})} coefficients.
257
+ '''
258
+ def _c4(nC4, C4x):
259
+ i, x, e = 0, _1_0, eps
260
+ _p = _polynomial
261
+ for r in range(nC4, 0, -1):
262
+ j = i + r
263
+ yield _p(e, C4x, i, j) * x
264
+ x *= e
265
+ i = j
266
+ # assert i == (nC4 * (nC4 + 1)) // 2
267
+
268
+ return tuple(_c4(self._nC4, self._C4x))
269
+
270
+ def _C4f_k2(self, k2): # in ._GDictInverse and gxline._GeodesicLineExact._C4a
271
+ '''(INTERNAL) Compute C{eps} from B{C{k2}} and invoke C{C4f}.
272
+ '''
273
+ return self.C4f(k2 / fsumf_(_2_0, sqrt(k2 + _1_0) * _2_0, k2))
274
+
275
+ @Property
276
+ def C4order(self):
277
+ '''Get the series expansion order (C{int}, 24, 27 or 30).
278
+ '''
279
+ return self._nC4
280
+
281
+ @C4order.setter # PYCHOK .setter!
282
+ def C4order(self, order):
283
+ '''Set the series expansion order (C{int}, 24, 27 or 30).
284
+
285
+ @raise GeodesicError: Invalid B{C{order}}.
286
+ '''
287
+ _xnC4(C4order=order)
288
+ if self._nC4 != order:
289
+ GeodesicExact._C4x._update(self)
290
+ _update_glXs(self) # zap cached _GeodesicLineExact attrs _B41, _C4a
291
+ self._nC4 = order
292
+
293
+ @deprecated_Property
294
+ def C4Order(self):
295
+ '''DEPRECATED, use property C{C4order}.
296
+ '''
297
+ return self.C4order
298
+
299
+ @C4Order.setter # PYCHOK .setter!
300
+ def C4Order(self, order):
301
+ '''DEPRECATED, use property C{C4order}.
302
+ '''
303
+ _xnC4(C4Order=order)
304
+ self.C4order = order
305
+
306
+ @Property_RO
307
+ def _C4x(self):
308
+ '''Get this ellipsoid's C{C4} coefficients, I{cached} tuple.
309
+
310
+ @see: Property L{C4order}.
311
+ '''
312
+ # see C4coeff() in GeographicLib.src.GeodesicExactC4.cpp
313
+ def _C4(nC4):
314
+ i, n, cs = 0, self.n, _C4coeffs(nC4)
315
+ _p = _polynomial
316
+ for r in range(nC4 + 1, 1, -1):
317
+ for j in range(1, r):
318
+ j = j + i # (j - i - 1) order of polynomial
319
+ yield _p(n, cs, i, j) / cs[j]
320
+ i = j + 1
321
+ # assert i == (nC4 * (nC4 + 1) * (nC4 + 5)) // 6
322
+
323
+ return tuple(_C4(self._nC4)) # 3rd flattening
324
+
325
+ @property_RO
326
+ def datum(self):
327
+ '''Get the datum (C{Datum}).
328
+ '''
329
+ return self._datum
330
+
331
+ def Direct(self, lat1, lon1, azi1, s12=0, outmask=Caps.STANDARD):
332
+ '''Solve the I{Direct} geodesic problem
333
+
334
+ @arg lat1: Latitude of the first point (C{degrees}).
335
+ @arg lon1: Longitude of the first point (C{degrees}).
336
+ @arg azi1: Azimuth at the first point (compass C{degrees}).
337
+ @arg s12: Distance between the points (C{meter}), can be negative.
338
+ @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
339
+ the quantities to be returned.
340
+
341
+ @return: A L{GDict} with up to 12 items C{lat1, lon1, azi1, lat2,
342
+ lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},
343
+ C{lon1}, C{azi1} and distance C{s12} always included.
344
+
345
+ @see: C++ U{GeodesicExact.Direct
346
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}
347
+ and Python U{Geodesic.Direct<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
348
+ '''
349
+ return self._GDictDirect(lat1, lon1, azi1, False, s12, outmask)
350
+
351
+ def Direct3(self, lat1, lon1, azi1, s12): # PYCHOK outmask
352
+ '''Return the destination lat, lon and reverse azimuth
353
+ (final bearing) in C{degrees}.
354
+
355
+ @return: L{Destination3Tuple}C{(lat, lon, final)}.
356
+ '''
357
+ r = self._GDictDirect(lat1, lon1, azi1, False, s12, Caps._AZIMUTH_LATITUDE_LONGITUDE)
358
+ return Destination3Tuple(r.lat2, r.lon2, r.azi2) # no iteration
359
+
360
+ def _DirectLine(self, ll1, azi12, s12=0, **caps_name):
361
+ '''(INTERNAL) Short-cut version.
362
+ '''
363
+ return self.DirectLine(ll1.lat, ll1.lon, azi12, s12, **caps_name)
364
+
365
+ def DirectLine(self, lat1, lon1, azi1, s12, caps=Caps.STANDARD, name=NN):
366
+ '''Define a L{GeodesicLineExact} in terms of the I{direct} geodesic problem and as distance.
367
+
368
+ @arg lat1: Latitude of the first point (C{degrees}).
369
+ @arg lon1: Longitude of the first point (C{degrees}).
370
+ @arg azi1: Azimuth at the first point (compass C{degrees}).
371
+ @arg s12: Distance between the points (C{meter}), can be negative.
372
+ @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
373
+ the capabilities the L{GeodesicLineExact} instance
374
+ should possess, i.e., which quantities can be
375
+ returned by calls to L{GeodesicLineExact.Position}.
376
+
377
+ @return: A L{GeodesicLineExact} instance.
378
+
379
+ @note: The third point of the L{GeodesicLineExact} is set to correspond
380
+ to the second point of the I{Inverse} geodesic problem.
381
+
382
+ @note: Latitude B{C{lat1}} should in the range C{[-90, +90]}.
383
+
384
+ @see: C++ U{GeodesicExact.DirectLine
385
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and
386
+ Python U{Geodesic.DirectLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
387
+ '''
388
+ return self._GenDirectLine(lat1, lon1, azi1, False, s12, caps, name=name)
389
+
390
+ def _dn(self, sbet, cbet): # in gxline._GeodesicLineExact.__init__
391
+ '''(INTERNAL) Helper.
392
+ '''
393
+ if self.f < 0: # PYCHOK no cover
394
+ dn = sqrt(_1_0 - cbet**2 * self.e2) / self.f1
395
+ else:
396
+ dn = sqrt(_1_0 + sbet**2 * self.ep2)
397
+ return dn
398
+
399
+ @Property_RO
400
+ def e2(self):
401
+ '''Get the ellipsoid's I{(1st) eccentricity squared} (C{float}), M{f * (2 - f)}.
402
+ '''
403
+ return self.ellipsoid.e2
404
+
405
+ @Property_RO
406
+ def _e2a2(self):
407
+ '''(INTERNAL) Cache M{E.e2 * E.a2}.
408
+ '''
409
+ return self.e2 * self.ellipsoid.a2
410
+
411
+ @Property_RO
412
+ def _e2_f1(self):
413
+ '''(INTERNAL) Cache M{E.e2 * E.f1}.
414
+ '''
415
+ return self.e2 / self.f1
416
+
417
+ @Property_RO
418
+ def _eF(self):
419
+ '''(INTERNAL) Get the elliptic function, aka C{.E}.
420
+ '''
421
+ return _MODS.elliptic.Elliptic(k2=-self.ep2)
422
+
423
+ def _eF_reset_cHe2_f1(self, x, y):
424
+ '''(INTERNAL) Reset elliptic function and return M{cH * e2 / f1 * ...}.
425
+ '''
426
+ self._eF_reset_k2(x)
427
+ return y * self._eF.cH * self._e2_f1
428
+
429
+ def _eF_reset_k2(self, x):
430
+ '''(INTERNAL) Reset elliptic function and return C{k2}.
431
+ '''
432
+ ep2 = self.ep2
433
+ k2 = x**2 * ep2 # see .gxline._GeodesicLineExact._eF
434
+ self._eF.reset(k2=-k2, alpha2=-ep2) # kp2, alphap2 defaults
435
+ _update_glXs(self) # zap cached/memoized _GeodesicLineExact attrs
436
+ return k2
437
+
438
+ @Property_RO
439
+ def ellipsoid(self):
440
+ '''Get the ellipsoid (C{Ellipsoid}).
441
+ '''
442
+ return self.datum.ellipsoid
443
+
444
+ @Property_RO
445
+ def ep2(self):
446
+ '''Get the ellipsoid's I{2nd eccentricity squared} (C{float}), M{e2 / (1 - e2)}.
447
+ '''
448
+ return self.ellipsoid.e22 # == self.e2 / self.f1**2
449
+
450
+ e22 = ep2 # for ellipsoid compatibility
451
+
452
+ @Property_RO
453
+ def _eTOL2(self):
454
+ '''(INTERNAL) The si12 threshold for "really short".
455
+ '''
456
+ return _eTOL2(self.f)
457
+
458
+ @Property_RO
459
+ def flattening(self):
460
+ '''Get the C{ellipsoid}'s I{flattening} (C{scalar}), M{(a - b) / a}, C{0} for spherical, negative for prolate.
461
+ '''
462
+ return self.ellipsoid.f
463
+
464
+ f = flattening
465
+
466
+ @Property_RO
467
+ def f1(self): # in .css.CassiniSoldner.reset
468
+ '''Get the C{ellipsoid}'s I{1 - flattening} (C{float}).
469
+ '''
470
+ return self.ellipsoid.f1
471
+
472
+ @Property_RO
473
+ def _f180(self):
474
+ '''(INTERNAL) Cached/memoized.
475
+ '''
476
+ return self.f * _180_0
477
+
478
+ def _GDictDirect(self, lat1, lon1, azi1, arcmode, s12_a12, outmask=Caps.STANDARD):
479
+ '''(INTERNAL) As C{_GenDirect}, but returning a L{GDict}.
480
+
481
+ @return: A L{GDict} ...
482
+ '''
483
+ C = outmask if arcmode else (outmask | Caps.DISTANCE_IN)
484
+ glX = self.Line(lat1, lon1, azi1, C | Caps.LINE_OFF)
485
+ return glX._GDictPosition(arcmode, s12_a12, outmask)
486
+
487
+ def _GDictInverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD): # MCCABE 33, 41 vars
488
+ '''(INTERNAL) As C{_GenInverse}, but returning a L{GDict}.
489
+
490
+ @return: A L{GDict} ...
491
+ '''
492
+ Cs = Caps
493
+ if self._debug: # PYCHOK no cover
494
+ outmask |= Cs._DEBUG_INVERSE & self._debug
495
+ outmask &= Cs._OUT_MASK # incl. _SALPs_CALPs and _DEBUG_
496
+ # compute longitude difference carefully (with _diff182):
497
+ # result is in [-180, +180] but -180 is only for west-going
498
+ # geodesics, +180 is for east-going and meridional geodesics
499
+ lon12, lon12s = _diff182(lon1, lon2)
500
+ # see C{result} from geographiclib.geodesic.Inverse
501
+ if (outmask & Cs.LONG_UNROLL): # == (lon1 + lon12) + lon12s
502
+ r = GDict(lon1=lon1, lon2=fsumf_(lon1, lon12, lon12s))
503
+ else:
504
+ r = GDict(lon1=_norm180(lon1), lon2=_norm180(lon2))
505
+ if _K_2_0: # GeographicLib 2.0
506
+ # make longitude difference positive
507
+ lon12, lon_ = _unsigned2(lon12)
508
+ if lon_:
509
+ lon12s = -lon12s
510
+ lam12 = radians(lon12)
511
+ # calculate sincosd(_around(lon12 + correction))
512
+ slam12, clam12 = _sincos2de(lon12, lon12s)
513
+ # supplementary longitude difference
514
+ lon12s = fsumf_(_180_0, -lon12, -lon12s)
515
+ else: # GeographicLib 1.52
516
+ # make longitude difference positive and if very close
517
+ # to being on the same half-meridian, then make it so.
518
+ if lon12 < 0: # _signBit(lon12)
519
+ lon_, lon12 = True, -_around(lon12)
520
+ lon12s = _around(fsumf_(_180_0, -lon12, lon12s))
521
+ else:
522
+ lon_, lon12 = False, _around(lon12)
523
+ lon12s = _around(fsumf_(_180_0, -lon12, -lon12s))
524
+ lam12 = radians(lon12)
525
+ if lon12 > _90_0:
526
+ slam12, clam12 = _sincos2d(lon12s)
527
+ clam12 = -clam12
528
+ else:
529
+ slam12, clam12 = _sincos2(lam12)
530
+ # If really close to the equator, treat as on equator.
531
+ lat1 = _around(_fix90(lat1))
532
+ lat2 = _around(_fix90(lat2))
533
+ r.set_(lat1=lat1, lat2=lat2)
534
+ # Swap points so that point with higher (abs) latitude is
535
+ # point 1. If one latitude is a NAN, then it becomes lat1.
536
+ swap_ = fabs(lat1) < fabs(lat2) or isnan(lat2)
537
+ if swap_:
538
+ lat1, lat2 = lat2, lat1
539
+ lon_ = not lon_
540
+ if _signBit(lat1):
541
+ lat_ = False # note, False
542
+ else: # make lat1 <= -0
543
+ lat_ = True # note, True
544
+ lat1, lat2 = -lat1, -lat2
545
+ # Now 0 <= lon12 <= 180, -90 <= lat1 <= -0 and lat1 <= lat2 <= -lat1
546
+ # and lat_, lon_, swap_ register the transformation to bring the
547
+ # coordinates to this canonical form, where False means no change
548
+ # made. We make these transformations so that there are few cases
549
+ # to check, e.g., on verifying quadrants in atan2. In addition,
550
+ # this enforces some symmetries in the results returned.
551
+
552
+ # Initialize for the meridian. No longitude calculation is
553
+ # done in this case to let the parameter default to 0.
554
+ sbet1, cbet1 = self._sinf1cos2d(lat1)
555
+ sbet2, cbet2 = self._sinf1cos2d(lat2)
556
+ # If cbet1 < -sbet1, then cbet2 - cbet1 is a sensitive measure
557
+ # of the |bet1| - |bet2|. Alternatively (cbet1 >= -sbet1),
558
+ # abs(sbet2) + sbet1 is a better measure. This logic is used
559
+ # in assigning calp2 in _Lambda6. Sometimes these quantities
560
+ # vanish and in that case we force bet2 = +/- bet1 exactly. An
561
+ # example where is is necessary is the inverse problem
562
+ # 48.522876735459 0 -48.52287673545898293 179.599720456223079643
563
+ # which failed with Visual Studio 10 (Release and Debug)
564
+ if cbet1 < -sbet1:
565
+ if cbet2 == cbet1:
566
+ sbet2 = copysign(sbet1, sbet2)
567
+ elif fabs(sbet2) == -sbet1:
568
+ cbet2 = cbet1
569
+
570
+ p = _PDict(sbet1=sbet1, cbet1=cbet1, dn1=self._dn(sbet1, cbet1),
571
+ sbet2=sbet2, cbet2=cbet2, dn2=self._dn(sbet2, cbet2))
572
+
573
+ _meridian = _b = True # i.e. not meridian, not b
574
+ if lat1 == -90 or slam12 == 0:
575
+ # Endpoints are on a single full meridian,
576
+ # so the geodesic might lie on a meridian.
577
+ salp1, calp1 = slam12, clam12 # head to target lon
578
+ salp2, calp2 = _0_0, _1_0 # then head north
579
+ # tan(bet) = tan(sig) * cos(alp)
580
+ p.set_sigs(sbet1, calp1 * cbet1, sbet2, calp2 * cbet2)
581
+ # sig12 = sig2 - sig1
582
+ sig12 = _atan12(sbet1, p.csig1, sbet2, p.csig2, sineg0=True) # PYCHOK csig*
583
+ s12x, m12x, _, \
584
+ M12, M21 = self._Length5(sig12, outmask | Cs.REDUCEDLENGTH, p)
585
+ # Add the check for sig12 since zero length geodesics
586
+ # might yield m12 < 0. Test case was
587
+ # echo 20.001 0 20.001 0 | GeodSolve -i
588
+ # In fact, we will have sig12 > PI/2 for meridional
589
+ # geodesic which is not a shortest path.
590
+ if m12x >= 0 or sig12 < _1_0:
591
+ # Need at least 2 to handle 90 0 90 180
592
+ # Prevent negative s12 or m12 from geographiclib 1.52
593
+ if sig12 < _TINY3 or (sig12 < _TOL0 and (s12x < 0 or m12x < 0)):
594
+ sig12 = m12x = s12x = _0_0
595
+ else:
596
+ _b = False # apply .b to s12x, m12x
597
+ _meridian = False
598
+ C = 1
599
+ # else: # m12 < 0, prolate and too close to anti-podal
600
+ # _meridian = True
601
+ a12 = _0_0 # if _b else degrees(sig12)
602
+
603
+ if _meridian:
604
+ _b = sbet1 == 0 and (self.f <= 0 or lon12s >= self._f180) # and sbet2 == 0
605
+ if _b: # Geodesic runs along equator
606
+ calp1 = calp2 = _0_0
607
+ salp1 = salp2 = _1_0
608
+ sig12 = lam12 / self.f1 # == omg12
609
+ somg12, comg12 = _sincos2(sig12)
610
+ m12x = self.b * somg12
611
+ s12x = self.a * lam12
612
+ M12 = M21 = comg12
613
+ a12 = lon12 / self.f1
614
+ C = 2
615
+ else:
616
+ # Now point1 and point2 belong within a hemisphere bounded by a
617
+ # meridian and geodesic is neither meridional or equatorial.
618
+ p.set_(slam12=slam12, clam12=clam12)
619
+ # Figure a starting point for Newton's method
620
+ sig12, salp1, calp1, \
621
+ salp2, calp2, dnm = self._InverseStart6(lam12, p)
622
+ if sig12 is None: # use Newton's method
623
+ # pre-compute the constant _Lambda6 term, once
624
+ p.set_(bet12=None if cbet2 == cbet1 and fabs(sbet2) == -sbet1 else
625
+ (((cbet1 + cbet2) * (cbet2 - cbet1)) if cbet1 < -sbet1 else
626
+ ((sbet1 + sbet2) * (sbet1 - sbet2))))
627
+ sig12, salp1, calp1, \
628
+ salp2, calp2, domg12 = self._Newton6(salp1, calp1, p)
629
+ s12x, m12x, _, M12, M21 = self._Length5(sig12, outmask, p)
630
+ if (outmask & Cs.AREA):
631
+ # omg12 = lam12 - domg12
632
+ s, c = _sincos2(domg12)
633
+ somg12, comg12 = _sincos12(s, c, slam12, clam12)
634
+ C = 3 # Newton
635
+ else: # from _InverseStart6: dnm, salp*, calp*
636
+ C = 4 # Short lines
637
+ s, c = _sincos2(sig12 / dnm)
638
+ m12x = dnm**2 * s
639
+ s12x = dnm * sig12
640
+ M12 = M21 = c
641
+ if (outmask & Cs.AREA):
642
+ somg12, comg12 = _sincos2(lam12 / (self.f1 * dnm))
643
+
644
+ else: # _meridian is False
645
+ somg12 = comg12 = NAN
646
+
647
+ r.set_(a12=a12 if _b else degrees(sig12)) # in [0, 180]
648
+
649
+ if (outmask & Cs.DISTANCE):
650
+ r.set_(s12=unsigned0(s12x if _b else (self.b * s12x)))
651
+
652
+ if (outmask & Cs.REDUCEDLENGTH):
653
+ r.set_(m12=unsigned0(m12x if _b else (self.b * m12x)))
654
+
655
+ if (outmask & Cs.GEODESICSCALE):
656
+ if swap_:
657
+ M12, M21 = M21, M12
658
+ r.set_(M12=unsigned0(M12),
659
+ M21=unsigned0(M21))
660
+
661
+ if (outmask & Cs.AREA):
662
+ S12 = self._InverseArea(_meridian, salp1, calp1,
663
+ salp2, calp2,
664
+ somg12, comg12, p)
665
+ if _xor(swap_, lat_, lon_):
666
+ S12 = -S12
667
+ r.set_(S12=unsigned0(S12))
668
+
669
+ if (outmask & (Cs.AZIMUTH | Cs._SALPs_CALPs)):
670
+ if swap_:
671
+ salp1, salp2 = salp2, salp1
672
+ calp1, calp2 = calp2, calp1
673
+ if _xor(swap_, lon_):
674
+ salp1, salp2 = -salp1, -salp2
675
+ if _xor(swap_, lat_):
676
+ calp1, calp2 = -calp1, -calp2
677
+
678
+ if (outmask & Cs.AZIMUTH):
679
+ r.set_(azi1=_atan2d(salp1, calp1),
680
+ azi2=_atan2d_reverse(salp2, calp2, reverse=outmask & Cs.REVERSE2))
681
+ if (outmask & Cs._SALPs_CALPs):
682
+ r.set_(salp1=salp1, calp1=calp1,
683
+ salp2=salp2, calp2=calp2)
684
+
685
+ if (outmask & Cs._DEBUG_INVERSE): # PYCHOK no cover
686
+ E, eF = self.ellipsoid, self._eF
687
+ p.set_(C=C, a=self.a, f=self.f, f1=self.f1,
688
+ e=E.e, e2=self.e2, ep2=self.ep2,
689
+ c2=E.c2, c2x=self.c2x,
690
+ eFcD=eF.cD, eFcE=eF.cE, eFcH=eF.cH,
691
+ eFk2=eF.k2, eFa2=eF.alpha2)
692
+ p.update(r) # r overrides p
693
+ r = p.toGDict()
694
+ return self._iter2tion(r, p)
695
+
696
+ def _GenDirect(self, lat1, lon1, azi1, arcmode, s12_a12, outmask=Caps.STANDARD):
697
+ '''(INTERNAL) The general I{Inverse} geodesic calculation.
698
+
699
+ @return: L{Direct9Tuple}C{(a12, lat2, lon2, azi2,
700
+ s12, m12, M12, M21, S12)}.
701
+ '''
702
+ r = self._GDictDirect(lat1, lon1, azi1, arcmode, s12_a12, outmask)
703
+ return r.toDirect9Tuple()
704
+
705
+ def _GenDirectLine(self, lat1, lon1, azi1, arcmode, s12_a12, caps, name=NN):
706
+ '''(INTERNAL) Helper for C{ArcDirectLine} and C{DirectLine}.
707
+
708
+ @return: A L{GeodesicLineExact} instance.
709
+ '''
710
+ azi1 = _norm180(azi1)
711
+ # guard against underflow in salp0. Also -0 is converted to +0.
712
+ s, c = _sincos2d(_around(azi1))
713
+ C = caps if arcmode else (caps | Caps.DISTANCE_IN)
714
+ return _GeodesicLineExact(self, lat1, lon1, azi1, C,
715
+ self._debug, s, c, name=name)._GenSet(arcmode, s12_a12)
716
+
717
+ def _GenInverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD):
718
+ '''(INTERNAL) The general I{Inverse} geodesic calculation.
719
+
720
+ @return: L{Inverse10Tuple}C{(a12, s12, salp1, calp1, salp2, calp2,
721
+ m12, M12, M21, S12)}.
722
+ '''
723
+ r = self._GDictInverse(lat1, lon1, lat2, lon2, outmask | Caps._SALPs_CALPs)
724
+ return r.toInverse10Tuple()
725
+
726
+ def _Inverse(self, ll1, ll2, wrap, **outmask):
727
+ '''(INTERNAL) Short-cut version, see .base.ellipsoidalDI.intersecant2.
728
+ '''
729
+ if wrap:
730
+ ll2 = _unrollon(ll1, _Wrap.point(ll2))
731
+ return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask)
732
+
733
+ def Inverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD):
734
+ '''Perform the I{Inverse} geodesic calculation.
735
+
736
+ @arg lat1: Latitude of the first point (C{degrees}).
737
+ @arg lon1: Longitude of the first point (C{degrees}).
738
+ @arg lat2: Latitude of the second point (C{degrees}).
739
+ @arg lon2: Longitude of the second point (C{degrees}).
740
+ @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
741
+ the quantities to be returned.
742
+
743
+ @return: A L{GDict} with up to 12 items C{lat1, lon1, azi1, lat2,
744
+ lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},
745
+ C{lon1}, C{azi1} and distance C{s12} always included.
746
+
747
+ @note: The third point of the L{GeodesicLineExact} is set to correspond
748
+ to the second point of the I{Inverse} geodesic problem.
749
+
750
+ @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}.
751
+
752
+ @see: C++ U{GeodesicExact.InverseLine
753
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and
754
+ Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
755
+ '''
756
+ return self._GDictInverse(lat1, lon1, lat2, lon2, outmask)
757
+
758
+ def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False):
759
+ '''Return the non-negative, I{angular} distance in C{degrees}.
760
+
761
+ @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
762
+ B{C{lat2}} and B{C{lon2}} (C{bool}).
763
+ '''
764
+ # see .FrechetKarney.distance, .HausdorffKarney._distance
765
+ # and .HeightIDWkarney._distances
766
+ if wrap:
767
+ _, lat2, lon2 = _Wrap.latlon3(lat1, lat2, lon2, True) # _Geodesic.LONG_UNROLL
768
+ return fabs(self._GDictInverse(lat1, lon1, lat2, lon2, Caps._ANGLE_ONLY).a12)
769
+
770
+ def Inverse3(self, lat1, lon1, lat2, lon2): # PYCHOK outmask
771
+ '''Return the distance in C{meter} and the forward and
772
+ reverse azimuths (initial and final bearing) in C{degrees}.
773
+
774
+ @return: L{Distance3Tuple}C{(distance, initial, final)}.
775
+ '''
776
+ r = self._GDictInverse(lat1, lon1, lat2, lon2, Caps.AZIMUTH_DISTANCE)
777
+ return Distance3Tuple(r.s12, wrap360(r.azi1), wrap360(r.azi2),
778
+ iteration=r.iteration)
779
+
780
+ def _InverseLine(self, ll1, ll2, wrap, **caps_name):
781
+ '''(INTERNAL) Short-cut version.
782
+ '''
783
+ if wrap:
784
+ ll2 = _unrollon(ll1, _Wrap.point(ll2))
785
+ return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name)
786
+
787
+ def InverseLine(self, lat1, lon1, lat2, lon2, caps=Caps.STANDARD, name=NN):
788
+ '''Define a L{GeodesicLineExact} in terms of the I{Inverse} geodesic problem.
789
+
790
+ @arg lat1: Latitude of the first point (C{degrees}).
791
+ @arg lon1: Longitude of the first point (C{degrees}).
792
+ @arg lat2: Latitude of the second point (C{degrees}).
793
+ @arg lon2: Longitude of the second point (C{degrees}).
794
+ @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
795
+ the capabilities the L{GeodesicLineExact} instance
796
+ should possess, i.e., which quantities can be
797
+ returned by calls to L{GeodesicLineExact.Position}
798
+ and L{GeodesicLineExact.ArcPosition}.
799
+
800
+ @return: A L{GeodesicLineExact} instance.
801
+
802
+ @note: The third point of the L{GeodesicLineExact} is set to correspond
803
+ to the second point of the I{Inverse} geodesic problem.
804
+
805
+ @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}.
806
+
807
+ @see: C++ U{GeodesicExact.InverseLine
808
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and
809
+ Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
810
+ '''
811
+ Cs = Caps
812
+ r = self._GDictInverse(lat1, lon1, lat2, lon2, Cs._SALPs_CALPs) # No need for AZIMUTH
813
+ C = (caps | Cs.DISTANCE) if (caps & (Cs.DISTANCE_IN & Cs._OUT_MASK)) else caps
814
+ azi1 = _atan2d(r.salp1, r.calp1)
815
+ return _GeodesicLineExact(self, lat1, lon1, azi1, C, # ensure a12 is distance
816
+ self._debug, r.salp1, r.calp1, name=name)._GenSet(True, r.a12)
817
+
818
+ def _InverseArea(self, _meridian, salp1, calp1, # PYCHOK 9 args
819
+ salp2, calp2,
820
+ somg12, comg12, p):
821
+ '''(INTERNAL) Split off from C{_GDictInverse} to reduce complexity/length.
822
+
823
+ @return: Area C{S12}.
824
+ '''
825
+ # from _Lambda6: sin(alp1) * cos(bet1) = sin(alp0), calp0 > 0
826
+ salp0, calp0 = _sin1cos2(salp1, calp1, p.sbet1, p.cbet1)
827
+ A4 = calp0 * salp0
828
+ if A4:
829
+ # from _Lambda6: tan(bet) = tan(sig) * cos(alp)
830
+ k2 = calp0**2 * self.ep2
831
+ C4a = self._C4f_k2(k2)
832
+ B41 = _cosSeries(C4a, *_norm2(p.sbet1, calp1 * p.cbet1))
833
+ B42 = _cosSeries(C4a, *_norm2(p.sbet2, calp2 * p.cbet2))
834
+ # multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0)
835
+ A4 *= self._e2a2
836
+ S12 = A4 * (B42 - B41)
837
+ else: # avoid problems with indeterminate sig1, sig2 on equator
838
+ A4 = B41 = B42 = k2 = S12 = _0_0
839
+
840
+ if (_meridian and # omg12 < 3/4 * PI
841
+ comg12 > -_SQRT2_2 and # lon diff not too big
842
+ (p.sbet2 - p.sbet1) < _1_75): # lat diff not too big
843
+ # use tan(Gamma/2) = tan(omg12/2) *
844
+ # (tan(bet1/2) + tan(bet2/2)) /
845
+ # (tan(bet1/2) * tan(bet2/2) + 1))
846
+ # with tan(x/2) = sin(x) / (1 + cos(x))
847
+ dbet1 = p.cbet1 + _1_0
848
+ dbet2 = p.cbet2 + _1_0
849
+ domg12 = comg12 + _1_0
850
+ salp12 = (p.sbet1 * dbet2 + dbet1 * p.sbet2) * somg12
851
+ calp12 = (p.sbet1 * p.sbet2 + dbet1 * dbet2) * domg12
852
+ alp12 = atan2(salp12, calp12) * _2_0
853
+ else:
854
+ # alp12 = alp2 - alp1, used in atan2, no need to normalize
855
+ salp12, calp12 = _sincos12(salp1, calp1, salp2, calp2)
856
+ # The right thing appears to happen if alp1 = +/-180 and
857
+ # alp2 = 0, viz salp12 = -0 and alp12 = -180. However,
858
+ # this depends on the sign being attached to 0 correctly.
859
+ # Following ensures the correct behavior.
860
+ if salp12 == 0 and calp12 < 0:
861
+ alp12 = _copysign(PI, calp1)
862
+ else:
863
+ alp12 = atan2(salp12, calp12)
864
+
865
+ p.set_(alp12=alp12, A4=A4, B41=B41, B42=B42, k2=k2)
866
+ return S12 + self.c2x * alp12
867
+
868
+ def _InverseStart6(self, lam12, p):
869
+ '''(INTERNAL) Return a starting point for Newton's method in
870
+ C{salp1} and C{calp1} indicated by C{sig12=None}. If
871
+ Newton's method doesn't need to be used, return also
872
+ C{salp2}, C{calp2}, C{dnm} and C{sig12} non-C{None}.
873
+
874
+ @return: 6-Tuple C{(sig12, salp1, calp1, salp2, calp2, dnm)}
875
+ and C{p.set_sigs} updated for Newton, C{sig12=None}.
876
+ '''
877
+ sig12 = None # use Newton
878
+ salp1 = calp1 = salp2 = calp2 = dnm = NAN
879
+
880
+ # bet12 = bet2 - bet1 in [0, PI)
881
+ sbet12, cbet12 = _sincos12(p.sbet1, p.cbet1, p.sbet2, p.cbet2)
882
+ shortline = cbet12 >= 0 and sbet12 < _0_5 and (p.cbet2 * lam12) < _0_5
883
+ if shortline:
884
+ # sin((bet1 + bet2)/2)^2 = (sbet1 + sbet2)^2 / (
885
+ # (cbet1 + cbet2)^2 + (sbet1 + sbet2)^2)
886
+ t = (p.sbet1 + p.sbet2)**2
887
+ s = t / ((p.cbet1 + p.cbet2)**2 + t)
888
+ dnm = sqrt(_1_0 + self.ep2 * s)
889
+ somg12, comg12 = _sincos2(lam12 / (self.f1 * dnm))
890
+ else:
891
+ somg12, comg12 = p.slam12, p.clam12
892
+
893
+ # bet12a = bet2 + bet1 in (-PI, 0], note -sbet1
894
+ sbet12a, cbet12a = _sincos12(-p.sbet1, p.cbet1, p.sbet2, p.cbet2)
895
+
896
+ c = fabs(comg12) + _1_0 # == (1 - comg12) if comg12 < 0
897
+ s = somg12**2 / c
898
+ t = p.sbet1 * p.cbet2 * s
899
+ salp1 = p.cbet2 * somg12
900
+ calp1 = (sbet12a - t) if comg12 < 0 else (sbet12 + t)
901
+
902
+ ssig12 = _hypot(salp1, calp1)
903
+ csig12 = p.sbet1 * p.sbet2 + p.cbet1 * p.cbet2 * comg12
904
+
905
+ if shortline and ssig12 < self._eTOL2: # really short lines
906
+ t = c if comg12 < 0 else s
907
+ salp2, calp2 = _norm2(somg12 * p.cbet1,
908
+ sbet12 - p.cbet1 * p.sbet2 * t)
909
+ sig12 = atan2(ssig12, csig12) # do not use Newton
910
+
911
+ elif (self._n_0_1 or # Skip astroid calc if too eccentric
912
+ csig12 >= 0 or ssig12 >= (p.cbet1**2 * self._n6PI)):
913
+ pass # nothing to do, 0th order spherical approximation OK
914
+
915
+ else:
916
+ # Scale lam12 and bet2 to x, y coordinate system where antipodal
917
+ # point is at origin and singular point is at y = 0, x = -1
918
+ lam12x = atan2(-p.slam12, -p.clam12) # lam12 - PI
919
+ f = self.f
920
+ if f < 0: # PYCHOK no cover
921
+ # ssig1=sbet1, csig1=-cbet1, ssig2=sbet2, csig2=cbet2
922
+ p.set_sigs(p.sbet1, -p.cbet1, p.sbet2, p.cbet2)
923
+ # if lon12 = 180, this repeats a calculation made in Inverse
924
+ _, m12b, m0, _, _ = self._Length5(atan2(sbet12a, cbet12a) + PI,
925
+ Caps.REDUCEDLENGTH, p)
926
+ t = p.cbet1 * PI # x = dlat, y = dlon
927
+ x = m12b / (t * p.cbet2 * m0) - _1_0
928
+ sca = (sbet12a / (x * p.cbet1)) if x < -_0_01 else (-f * t)
929
+ y = lam12x / sca
930
+ else: # f >= 0, however f == 0 does not get here
931
+ sca = self._eF_reset_cHe2_f1(p.sbet1, p.cbet1 * _2_0)
932
+ x = lam12x / sca # dlon
933
+ y = sbet12a / (sca * p.cbet1) # dlat
934
+
935
+ if y > _TOL1 and x > -_THR1: # strip near cut
936
+ if f < 0: # PYCHOK no cover
937
+ calp1 = max( _0_0, x) if x > _TOL1 else max(_N_1_0, x)
938
+ salp1 = sqrt(_1_0 - calp1**2)
939
+ else:
940
+ salp1 = min( _1_0, -x)
941
+ calp1 = -sqrt(_1_0 - salp1**2)
942
+ else:
943
+ # Estimate alp1, by solving the astroid problem.
944
+ #
945
+ # Could estimate alpha1 = theta + PI/2, directly, i.e.,
946
+ # calp1 = y/k; salp1 = -x/(1+k); for _f >= 0
947
+ # calp1 = x/(1+k); salp1 = -y/k; for _f < 0 (need to check)
948
+ #
949
+ # However, it's better to estimate omg12 from astroid and use
950
+ # spherical formula to compute alp1. This reduces the mean
951
+ # number of Newton iterations for astroid cases from 2.24
952
+ # (min 0, max 6) to 2.12 (min 0, max 5). The changes in the
953
+ # number of iterations are as follows:
954
+ #
955
+ # change percent
956
+ # 1 5
957
+ # 0 78
958
+ # -1 16
959
+ # -2 0.6
960
+ # -3 0.04
961
+ # -4 0.002
962
+ #
963
+ # The histogram of iterations is (m = number of iterations
964
+ # estimating alp1 directly, n = number of iterations
965
+ # estimating via omg12, total number of trials = 148605):
966
+ #
967
+ # iter m n
968
+ # 0 148 186
969
+ # 1 13046 13845
970
+ # 2 93315 102225
971
+ # 3 36189 32341
972
+ # 4 5396 7
973
+ # 5 455 1
974
+ # 6 56 0
975
+ #
976
+ # omg12 is near PI, estimate work with omg12a = PI - omg12
977
+ k = _Astroid(x, y)
978
+ sca *= (y * (k + _1_0) / k) if f < 0 else \
979
+ (x * k / (k + _1_0))
980
+ s, c = _sincos2(-sca) # omg12a
981
+ # update spherical estimate of alp1 using omg12 instead of lam12
982
+ salp1 = p.cbet2 * s
983
+ calp1 = sbet12a - s * salp1 * p.sbet1 / (c + _1_0) # c = -c
984
+
985
+ # sanity check on starting guess. Backwards check allows NaN through.
986
+ salp1, calp1 = _norm2(salp1, calp1) if salp1 > 0 else (_1_0, _0_0)
987
+
988
+ return sig12, salp1, calp1, salp2, calp2, dnm
989
+
990
+ def _Lambda6(self, salp1, calp1, diffp, p):
991
+ '''(INTERNAL) Helper.
992
+
993
+ @return: 6-Tuple C{(lam12, sig12, salp2, calp2, domg12,
994
+ dlam12} and C{p.set_sigs} updated.
995
+ '''
996
+ eF = self._eF
997
+ f1 = self.f1
998
+
999
+ if p.sbet1 == calp1 == 0: # PYCHOK no cover
1000
+ # Break degeneracy of equatorial line
1001
+ calp1 = -_TINY
1002
+
1003
+ # sin(alp1) * cos(bet1) = sin(alp0), # calp0 > 0
1004
+ salp0, calp0 = _sin1cos2(salp1, calp1, p.sbet1, p.cbet1)
1005
+ # tan(bet1) = tan(sig1) * cos(alp1)
1006
+ # tan(omg1) = sin(alp0) * tan(sig1)
1007
+ # = sin(bet1) * tan(alp1)
1008
+ somg1 = salp0 * p.sbet1
1009
+ comg1 = calp1 * p.cbet1
1010
+ ssig1, csig1 = _norm2(p.sbet1, comg1)
1011
+ # Without normalization we have schi1 = somg1
1012
+ cchi1 = f1 * p.dn1 * comg1
1013
+
1014
+ # Enforce symmetries in the case abs(bet2) = -bet1.
1015
+ # Need to be careful about this case, since this can
1016
+ # yield singularities in the Newton iteration.
1017
+ # sin(alp2) * cos(bet2) = sin(alp0)
1018
+ salp2 = (salp0 / p.cbet2) if p.cbet2 != p.cbet1 else salp1
1019
+ # calp2 = sqrt(1 - sq(salp2))
1020
+ # = sqrt(sq(calp0) - sq(sbet2)) / cbet2
1021
+ # and subst for calp0 and rearrange to give (choose
1022
+ # positive sqrt to give alp2 in [0, PI/2]).
1023
+ calp2 = fabs(calp1) if p.bet12 is None else (
1024
+ sqrt((calp1 * p.cbet1)**2 + p.bet12) / p.cbet2)
1025
+ # tan(bet2) = tan(sig2) * cos(alp2)
1026
+ # tan(omg2) = sin(alp0) * tan(sig2).
1027
+ somg2 = salp0 * p.sbet2
1028
+ comg2 = calp2 * p.cbet2
1029
+ ssig2, csig2 = _norm2(p.sbet2, comg2)
1030
+ # without normalization we have schi2 = somg2
1031
+ cchi2 = f1 * p.dn2 * comg2
1032
+
1033
+ # omg12 = omg2 - omg1, limit to [0, PI]
1034
+ somg12, comg12 = _sincos12(somg1, comg1, somg2, comg2, sineg0=True)
1035
+ # chi12 = chi2 - chi1, limit to [0, PI]
1036
+ schi12, cchi12 = _sincos12(somg1, cchi1, somg2, cchi2, sineg0=True)
1037
+
1038
+ p.set_sigs(ssig1, csig1, ssig2, csig2)
1039
+ # sig12 = sig2 - sig1, limit to [0, PI]
1040
+ sig12 = _atan12(ssig1, csig1, ssig2, csig2, sineg0=True)
1041
+
1042
+ eta12 = self._eF_reset_cHe2_f1(calp0, salp0) * _2__PI # then ...
1043
+ eta12 *= fsum1f_(eF.deltaH(*p.sncndn2),
1044
+ -eF.deltaH(*p.sncndn1), sig12)
1045
+ # eta = chi12 - lam12
1046
+ lam12 = _atan12(p.slam12, p.clam12, schi12, cchi12) - eta12
1047
+ # domg12 = chi12 - omg12 - deta12
1048
+ domg12 = _atan12( somg12, comg12, schi12, cchi12) - eta12
1049
+
1050
+ dlam12 = NAN # dv > 0 in ._Newton6
1051
+ if diffp:
1052
+ d = calp2 * p.cbet2
1053
+ if d:
1054
+ _, dlam12, _, _, _ = self._Length5(sig12, Caps.REDUCEDLENGTH, p)
1055
+ dlam12 *= f1 / d
1056
+ elif p.sbet1:
1057
+ dlam12 = -f1 * p.dn1 * _2_0 / p.sbet1
1058
+
1059
+ # p.set_(deta12=-eta12, lam12=lam12)
1060
+ return lam12, sig12, salp2, calp2, domg12, dlam12
1061
+
1062
+ def _Length5(self, sig12, outmask, p):
1063
+ '''(INTERNAL) Return M{m12b = (reduced length) / self.b} and
1064
+ calculate M{s12b = distance / self.b} and M{m0}, the
1065
+ coefficient of secular term in expression for reduced
1066
+ length and the geodesic scales C{M12} and C{M21}.
1067
+
1068
+ @return: 5-Tuple C{(s12b, m12b, m0, M12, M21)}.
1069
+ '''
1070
+ s12b = m12b = m0 = M12 = M21 = NAN
1071
+
1072
+ Cs = Caps
1073
+ eF = self._eF
1074
+
1075
+ # outmask &= Cs._OUT_MASK
1076
+ if (outmask & Cs.DISTANCE):
1077
+ # Missing a factor of self.b
1078
+ s12b = eF.cE * _2__PI * fsum1f_(eF.deltaE(*p.sncndn2),
1079
+ -eF.deltaE(*p.sncndn1), sig12)
1080
+
1081
+ if (outmask & Cs._REDUCEDLENGTH_GEODESICSCALE):
1082
+ m0x = -eF.k2 * eF.cD * _2__PI
1083
+ J12 = -m0x * fsum1f_(eF.deltaD(*p.sncndn2),
1084
+ -eF.deltaD(*p.sncndn1), sig12)
1085
+ if (outmask & Cs.REDUCEDLENGTH):
1086
+ m0 = m0x
1087
+ # Missing a factor of self.b. Add parens around
1088
+ # (csig1 * ssig2) and (ssig1 * csig2) to ensure
1089
+ # accurate cancellation for coincident points.
1090
+ m12b = fsum1f_(p.dn2 * (p.csig1 * p.ssig2),
1091
+ -p.dn1 * (p.ssig1 * p.csig2),
1092
+ J12 * (p.csig1 * p.csig2))
1093
+ if (outmask & Cs.GEODESICSCALE):
1094
+ M12 = M21 = p.ssig1 * p.ssig2 + \
1095
+ p.csig1 * p.csig2
1096
+ t = (p.cbet1 - p.cbet2) * self.ep2 * \
1097
+ (p.cbet1 + p.cbet2) / (p.dn1 + p.dn2)
1098
+ M12 += (p.ssig2 * t + p.csig2 * J12) * p.ssig1 / p.dn1
1099
+ M21 -= (p.ssig1 * t + p.csig1 * J12) * p.ssig2 / p.dn2
1100
+
1101
+ return s12b, m12b, m0, M12, M21
1102
+
1103
+ def Line(self, lat1, lon1, azi1, caps=Caps.ALL, name=NN):
1104
+ '''Set up a L{GeodesicLineExact} to compute several points
1105
+ on a single geodesic.
1106
+
1107
+ @arg lat1: Latitude of the first point (C{degrees}).
1108
+ @arg lon1: Longitude of the first point (C{degrees}).
1109
+ @arg azi1: Azimuth at the first point (compass C{degrees}).
1110
+ @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
1111
+ the capabilities the L{GeodesicLineExact} instance
1112
+ should possess, i.e., which quantities can be
1113
+ returnedby calls to L{GeodesicLineExact.Position}
1114
+ and L{GeodesicLineExact.ArcPosition}.
1115
+
1116
+ @return: A L{GeodesicLineExact} instance.
1117
+
1118
+ @note: If the point is at a pole, the azimuth is defined by keeping
1119
+ B{C{lon1}} fixed, writing C{B{lat1} = ±(90 − ε)}, and taking
1120
+ the limit C{ε → 0+}.
1121
+
1122
+ @see: C++ U{GeodesicExact.Line
1123
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}
1124
+ and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
1125
+ '''
1126
+ return _GeodesicLineExact(self, lat1, lon1, azi1, caps, self._debug, name=name)
1127
+
1128
+ @Property_RO
1129
+ def n(self):
1130
+ '''Get the C{ellipsoid}'s I{3rd flattening} (C{scalar}), M{f / (2 - f) == (a - b) / (a + b)}.
1131
+ '''
1132
+ return self.ellipsoid.n
1133
+
1134
+ @Property_RO
1135
+ def _n_0_1(self):
1136
+ '''(INTERNAL) Cached once.
1137
+ '''
1138
+ return fabs(self.n) > _0_1
1139
+
1140
+ @Property_RO
1141
+ def _n6PI(self):
1142
+ '''(INTERNAL) Cached once.
1143
+ '''
1144
+ return fabs(self.n) * _6_0 * PI
1145
+
1146
+ def _Newton6(self, salp1, calp1, p):
1147
+ '''(INTERNAL) Split off from C{_GDictInverse} to reduce complexity/length.
1148
+
1149
+ @return: 6-Tuple C{(sig12, salp1, calp1, salp2, calp2, domg12)}
1150
+ and C{p.iter} and C{p.trip} updated.
1151
+ '''
1152
+ _abs = fabs
1153
+ # This is a straightforward solution of f(alp1) = lambda12(alp1) -
1154
+ # lam12 = 0 with one wrinkle. f(alp) has exactly one root in the
1155
+ # interval (0, PI) and its derivative is positive at the root.
1156
+ # Thus f(alp) is positive for alp > alp1 and negative for alp < alp1.
1157
+ # During the course of the iteration, a range (alp1a, alp1b) is
1158
+ # maintained which brackets the root and with each evaluation of
1159
+ # f(alp) the range is shrunk, if possible. Newton's method is
1160
+ # restarted whenever the derivative of f is negative (because the
1161
+ # new value of alp1 is then further from the solution) or if the
1162
+ # new estimate of alp1 lies outside (0,PI); in this case, the new
1163
+ # starting guess is taken to be (alp1a + alp1b) / 2.
1164
+ salp1a = salp1b = _TINY
1165
+ calp1a, calp1b = _1_0, _N_1_0
1166
+ MAXIT1, TOL0 = _MAXIT1, _TOL0
1167
+ HALF, TOLb = _0_5, _TOLb
1168
+ tripb, TOLv = False, TOL0
1169
+ for i in range(_MAXIT2):
1170
+ # 1/4 meridian = 10e6 meter and random input,
1171
+ # estimated max error in nm (nano meter, by
1172
+ # checking Inverse problem by Direct).
1173
+ #
1174
+ # max iterations
1175
+ # log2(b/a) error mean sd
1176
+ # -7 387 5.33 3.68
1177
+ # -6 345 5.19 3.43
1178
+ # -5 269 5.00 3.05
1179
+ # -4 210 4.76 2.44
1180
+ # -3 115 4.55 1.87
1181
+ # -2 69 4.35 1.38
1182
+ # -1 36 4.05 1.03
1183
+ # 0 15 0.01 0.13
1184
+ # 1 25 5.10 1.53
1185
+ # 2 96 5.61 2.09
1186
+ # 3 318 6.02 2.74
1187
+ # 4 985 6.24 3.22
1188
+ # 5 2352 6.32 3.44
1189
+ # 6 6008 6.30 3.45
1190
+ # 7 19024 6.19 3.30
1191
+ v, sig12, salp2, calp2, \
1192
+ domg12, dv = self._Lambda6(salp1, calp1, i < MAXIT1, p)
1193
+
1194
+ # 2 * _TOL0 is approximately 1 ulp [0, PI]
1195
+ # reversed test to allow escape with NaNs
1196
+ if tripb or _abs(v) < TOLv:
1197
+ break
1198
+ # update bracketing values
1199
+ if v > 0 and (i > MAXIT1 or (calp1 / salp1) > (calp1b / salp1b)):
1200
+ salp1b, calp1b = salp1, calp1
1201
+ elif v < 0 and (i > MAXIT1 or (calp1 / salp1) < (calp1a / salp1a)):
1202
+ salp1a, calp1a = salp1, calp1
1203
+
1204
+ if i < MAXIT1 and dv > 0:
1205
+ dalp1 = -v / dv
1206
+ if _abs(dalp1) < PI:
1207
+ s, c = _sincos2(dalp1)
1208
+ # nalp1 = alp1 + dalp1
1209
+ s, c = _sincos12(-s, c, salp1, calp1)
1210
+ if s > 0:
1211
+ salp1, calp1 = _norm2(s, c)
1212
+ # in some regimes we don't get quadratic convergence
1213
+ # because slope -> 0. So use convergence conditions
1214
+ # based on epsilon instead of sqrt(epsilon)
1215
+ TOLv = TOL0 if _abs(v) > _TOL016 else _TOL08
1216
+ continue
1217
+ TOLv = TOL0
1218
+ # Either dv was not positive or updated value was outside
1219
+ # legal range. Use the midpoint of the bracket as the next
1220
+ # estimate. This mechanism is not needed for the WGS84
1221
+ # ellipsoid, but it does catch problems with more eccentric
1222
+ # ellipsoids. Its efficacy is such for the WGS84 test set
1223
+ # with the starting guess set to alp1 = 90 deg: the WGS84
1224
+ # test set: mean = 5.21, stdev = 3.93, max = 24 and WGS84
1225
+ # with random input: mean = 4.74, stdev = 0.99
1226
+ salp1, calp1 = _norm2((salp1a + salp1b) * HALF,
1227
+ (calp1a + calp1b) * HALF)
1228
+ tripb = fsum1f_(calp1a, -calp1, _abs(salp1a - salp1)) < TOLb or \
1229
+ fsum1f_(calp1b, -calp1, _abs(salp1b - salp1)) < TOLb
1230
+ else:
1231
+ raise GeodesicError(Fmt.no_convergence(v, TOLv), txt=repr(self)) # self.toRepr()
1232
+
1233
+ p.set_(iter=i, trip=tripb) # like .geodsolve._GDictInvoke: iter NOT iteration!
1234
+ return sig12, salp1, calp1, salp2, calp2, domg12
1235
+
1236
+ Polygon = Area # for C{geographiclib} compatibility
1237
+
1238
+ def _sinf1cos2d(self, lat):
1239
+ '''(INTERNAL) Helper, also for C{_G_GeodesicLineExact}.
1240
+ '''
1241
+ sbet, cbet = _sincos2d(lat)
1242
+ # ensure cbet1 = +epsilon at poles; doing the fix on beta means
1243
+ # that sig12 will be <= 2*tiny for two points at the same pole
1244
+ sbet, cbet = _norm2(sbet * self.f1, cbet)
1245
+ return sbet, (cbet if fabs(cbet) > _TINY else _TINY)
1246
+
1247
+ def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
1248
+ '''Return this C{GeodesicExact} as string.
1249
+
1250
+ @kwarg prec: The C{float} precision, number of decimal digits (0..9).
1251
+ Trailing zero decimals are stripped for B{C{prec}} values
1252
+ of 1 and above, but kept for negative B{C{prec}} values.
1253
+ @kwarg sep: Separator to join (C{str}).
1254
+
1255
+ @return: Tuple items (C{str}).
1256
+ '''
1257
+ d = dict(ellipsoid=self.ellipsoid, C4order=self.C4order)
1258
+ return sep.join(pairs(d, prec=prec))
1259
+
1260
+
1261
+ class GeodesicLineExact(_GeodesicLineExact):
1262
+ '''A pure Python version of I{Karney}'s C++ class U{GeodesicLineExact
1263
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicLineExact.html>},
1264
+ modeled after I{Karney}'s Python class U{geodesicline.GeodesicLine<https://GitHub.com/
1265
+ geographiclib/geographiclib-python>}.
1266
+ '''
1267
+
1268
+ def __init__(self, geodesic, lat1, lon1, azi1, caps=Caps.STANDARD, name=NN):
1269
+ '''New L{GeodesicLineExact} instance, allowing points to be found along
1270
+ a geodesic starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi1}}.
1271
+
1272
+ @arg geodesic: The geodesic to use (L{GeodesicExact}).
1273
+ @arg lat1: Latitude of the first point (C{degrees}).
1274
+ @arg lon1: Longitude of the first point (C{degrees}).
1275
+ @arg azi1: Azimuth at the first points (compass C{degrees}).
1276
+ @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
1277
+ the capabilities the L{GeodesicLineExact} instance
1278
+ should possess, i.e., which quantities can be
1279
+ returned by calls to L{GeodesicLineExact.Position}
1280
+ and L{GeodesicLineExact.ArcPosition}.
1281
+ @kwarg name: Optional name (C{str}).
1282
+
1283
+ @raise TypeError: Invalid B{C{geodesic}}.
1284
+ '''
1285
+ _xinstanceof(GeodesicExact, geodesic=geodesic)
1286
+ if (caps & Caps.LINE_OFF): # copy to avoid updates
1287
+ geodesic = geodesic.copy(deep=False, name=NN(_UNDER_, geodesic.name))
1288
+ # _update_all(geodesic)
1289
+ _GeodesicLineExact.__init__(self, geodesic, lat1, lon1, azi1, caps, 0, name=name)
1290
+
1291
+
1292
+ def _Astroid(x, y):
1293
+ '''(INTERNAL) Solve M{k^4 + 2 * k^3 - (x^2 + y^2 - 1)
1294
+ * k^2 - (2 * k + 1) * y^2 = 0} for positive root k.
1295
+ '''
1296
+ p = x**2
1297
+ q = y**2
1298
+ r = fsumf_(_1_0, q, p, _N_2_0)
1299
+ if r > 0 or q:
1300
+ # avoid possible division by zero when r = 0
1301
+ # by multiplying s and t by r^3 and r, resp.
1302
+ S = p * q / _4_0 # S = r^3 * s
1303
+ if r:
1304
+ r = r / _6_0 # /= chokes PyChecker
1305
+ r3 = r**3
1306
+ T3 = r3 + S
1307
+ # discriminant of the quadratic equation for T3 is
1308
+ # zero on the evolute curve p^(1/3) + q^(1/3) = 1
1309
+ d = (r3 + T3) * S
1310
+ if d < 0:
1311
+ # T is complex, but u is defined for a real result
1312
+ a = atan2(sqrt(-d), -T3) / _3_0
1313
+ # There are 3 possible cube roots, choose the one which
1314
+ # avoids cancellation. Note d < 0 implies that r < 0.
1315
+ u = (cos(a) * _2_0 + _1_0) * r
1316
+ else:
1317
+ # pick the sign on the sqrt to maximize abs(T3) to
1318
+ # minimize loss of precision due to cancellation.
1319
+ if d:
1320
+ T3 += _copysign(sqrt(d), T3) # T3 = (r * t)^3
1321
+ # _cbrt always returns the real root, _cbrt(-8) = -2
1322
+ u = _cbrt(T3) # T = r * t
1323
+ if u: # T can be zero; but then r2 / T -> 0
1324
+ u += r**2 / u
1325
+ u += r
1326
+ elif S: # d == T3**2 == S**2: sqrt(d) == abs(S) == abs(T3)
1327
+ u = _cbrt(S * _2_0) # == T3 + _copysign(abs(S), T3)
1328
+ else:
1329
+ u = _0_0
1330
+ v = _hypot(u, y) # sqrt(u**2 + q)
1331
+ # avoid loss of accuracy when u < 0
1332
+ u = (q / (v - u)) if u < 0 else (v + u)
1333
+ w = (u - q) / (v + v) # positive?
1334
+ # rearrange expression for k to avoid loss of accuracy due to
1335
+ # subtraction, division by 0 impossible because u > 0, w >= 0
1336
+ k = u / (sqrt(w**2 + u) + w) # guaranteed positive
1337
+
1338
+ else: # q == 0 && r <= 0
1339
+ # y = 0 with |x| <= 1. Handle this case directly, for
1340
+ # y small, positive root is k = abs(y) / sqrt(1 - x^2)
1341
+ k = _0_0
1342
+
1343
+ return k
1344
+
1345
+
1346
+ def _C4coeffs(nC4): # in .geodesicx.__main__
1347
+ '''(INTERNAL) Get the C{C4_24}, C{_27} or C{_30} series coefficients.
1348
+ '''
1349
+ try: # from pygeodesy.geodesicx._C4_xx import _coeffs_xx as _coeffs
1350
+ _C4_xx = _DOT_(_MODS.geodesicx.__name__, _UNDER_('_C4', nC4))
1351
+ _coeffs = _MODS.getattr(_C4_xx, _UNDER_('_coeffs', nC4))
1352
+ except (AttributeError, ImportError, TypeError) as x:
1353
+ raise GeodesicError(nC4=nC4, cause=x)
1354
+ n = _xnC4(nC4=nC4)
1355
+ if len(_coeffs) != n: # double check
1356
+ raise GeodesicError(_coeffs=len(_coeffs), _xnC4=n, nC4=nC4)
1357
+ return _coeffs
1358
+
1359
+
1360
+ __all__ += _ALL_DOCS(GeodesicExact, GeodesicLineExact)
1361
+
1362
+ # **) MIT License
1363
+ #
1364
+ # Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1365
+ #
1366
+ # Permission is hereby granted, free of charge, to any person obtaining a
1367
+ # copy of this software and associated documentation files (the "Software"),
1368
+ # to deal in the Software without restriction, including without limitation
1369
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
1370
+ # and/or sell copies of the Software, and to permit persons to whom the
1371
+ # Software is furnished to do so, subject to the following conditions:
1372
+ #
1373
+ # The above copyright notice and this permission notice shall be included
1374
+ # in all copies or substantial portions of the Software.
1375
+ #
1376
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1377
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1378
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1379
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1380
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1381
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1382
+ # OTHER DEALINGS IN THE SOFTWARE.