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.
- PyGeodesy-24.3.24.dist-info/METADATA +272 -0
- PyGeodesy-24.3.24.dist-info/RECORD +115 -0
- PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
- PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
- pygeodesy/LICENSE +21 -0
- pygeodesy/__init__.py +615 -0
- pygeodesy/__main__.py +103 -0
- pygeodesy/albers.py +867 -0
- pygeodesy/auxilats/_CX_4.py +218 -0
- pygeodesy/auxilats/_CX_6.py +314 -0
- pygeodesy/auxilats/_CX_8.py +475 -0
- pygeodesy/auxilats/__init__.py +54 -0
- pygeodesy/auxilats/__main__.py +86 -0
- pygeodesy/auxilats/auxAngle.py +548 -0
- pygeodesy/auxilats/auxDLat.py +302 -0
- pygeodesy/auxilats/auxDST.py +296 -0
- pygeodesy/auxilats/auxLat.py +848 -0
- pygeodesy/auxilats/auxily.py +272 -0
- pygeodesy/azimuthal.py +1150 -0
- pygeodesy/basics.py +892 -0
- pygeodesy/booleans.py +2031 -0
- pygeodesy/cartesianBase.py +1062 -0
- pygeodesy/clipy.py +704 -0
- pygeodesy/constants.py +516 -0
- pygeodesy/css.py +660 -0
- pygeodesy/datums.py +752 -0
- pygeodesy/deprecated/__init__.py +61 -0
- pygeodesy/deprecated/bases.py +40 -0
- pygeodesy/deprecated/classes.py +262 -0
- pygeodesy/deprecated/consterns.py +54 -0
- pygeodesy/deprecated/datum.py +40 -0
- pygeodesy/deprecated/functions.py +375 -0
- pygeodesy/deprecated/nvector.py +48 -0
- pygeodesy/deprecated/rhumbBase.py +32 -0
- pygeodesy/deprecated/rhumbaux.py +33 -0
- pygeodesy/deprecated/rhumbsolve.py +33 -0
- pygeodesy/deprecated/rhumbx.py +33 -0
- pygeodesy/dms.py +986 -0
- pygeodesy/ecef.py +1348 -0
- pygeodesy/elevations.py +279 -0
- pygeodesy/ellipsoidalBase.py +1224 -0
- pygeodesy/ellipsoidalBaseDI.py +913 -0
- pygeodesy/ellipsoidalExact.py +343 -0
- pygeodesy/ellipsoidalGeodSolve.py +343 -0
- pygeodesy/ellipsoidalKarney.py +403 -0
- pygeodesy/ellipsoidalNvector.py +685 -0
- pygeodesy/ellipsoidalVincenty.py +590 -0
- pygeodesy/ellipsoids.py +2476 -0
- pygeodesy/elliptic.py +1198 -0
- pygeodesy/epsg.py +243 -0
- pygeodesy/errors.py +804 -0
- pygeodesy/etm.py +1190 -0
- pygeodesy/fmath.py +1013 -0
- pygeodesy/formy.py +1818 -0
- pygeodesy/frechet.py +865 -0
- pygeodesy/fstats.py +760 -0
- pygeodesy/fsums.py +1898 -0
- pygeodesy/gars.py +358 -0
- pygeodesy/geodesicw.py +581 -0
- pygeodesy/geodesicx/_C4_24.py +1699 -0
- pygeodesy/geodesicx/_C4_27.py +2395 -0
- pygeodesy/geodesicx/_C4_30.py +3301 -0
- pygeodesy/geodesicx/__init__.py +48 -0
- pygeodesy/geodesicx/__main__.py +91 -0
- pygeodesy/geodesicx/gx.py +1382 -0
- pygeodesy/geodesicx/gxarea.py +535 -0
- pygeodesy/geodesicx/gxbases.py +154 -0
- pygeodesy/geodesicx/gxline.py +669 -0
- pygeodesy/geodsolve.py +426 -0
- pygeodesy/geohash.py +914 -0
- pygeodesy/geoids.py +1884 -0
- pygeodesy/hausdorff.py +892 -0
- pygeodesy/heights.py +1155 -0
- pygeodesy/interns.py +687 -0
- pygeodesy/iters.py +545 -0
- pygeodesy/karney.py +919 -0
- pygeodesy/ktm.py +633 -0
- pygeodesy/latlonBase.py +1766 -0
- pygeodesy/lazily.py +960 -0
- pygeodesy/lcc.py +684 -0
- pygeodesy/ltp.py +1107 -0
- pygeodesy/ltpTuples.py +1563 -0
- pygeodesy/mgrs.py +721 -0
- pygeodesy/named.py +1324 -0
- pygeodesy/namedTuples.py +683 -0
- pygeodesy/nvectorBase.py +695 -0
- pygeodesy/osgr.py +781 -0
- pygeodesy/points.py +1686 -0
- pygeodesy/props.py +628 -0
- pygeodesy/resections.py +1048 -0
- pygeodesy/rhumb/__init__.py +46 -0
- pygeodesy/rhumb/aux_.py +397 -0
- pygeodesy/rhumb/bases.py +1148 -0
- pygeodesy/rhumb/ekx.py +563 -0
- pygeodesy/rhumb/solve.py +572 -0
- pygeodesy/simplify.py +647 -0
- pygeodesy/solveBase.py +472 -0
- pygeodesy/sphericalBase.py +724 -0
- pygeodesy/sphericalNvector.py +1264 -0
- pygeodesy/sphericalTrigonometry.py +1447 -0
- pygeodesy/streprs.py +627 -0
- pygeodesy/trf.py +2079 -0
- pygeodesy/triaxials.py +1484 -0
- pygeodesy/units.py +969 -0
- pygeodesy/unitsBase.py +349 -0
- pygeodesy/ups.py +538 -0
- pygeodesy/utily.py +1231 -0
- pygeodesy/utm.py +762 -0
- pygeodesy/utmups.py +318 -0
- pygeodesy/utmupsBase.py +517 -0
- pygeodesy/vector2d.py +785 -0
- pygeodesy/vector3d.py +968 -0
- pygeodesy/vector3dBase.py +1049 -0
- pygeodesy/webmercator.py +383 -0
- pygeodesy/wgrs.py +439 -0
pygeodesy/rhumb/bases.py
ADDED
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
u'''(INTERNAL) base classes C{RhumbBase} and C{RhumbLineBase}, pure Python version of I{Karney}'s
|
|
5
|
+
C++ classes U{Rhumb<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Rhumb.html>}
|
|
6
|
+
and U{RhumbLine<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1RhumbLine.html>}
|
|
7
|
+
from I{GeographicLib versions 2.0} and I{2.2} and I{Karney}'s C++ example U{Rhumb intersect
|
|
8
|
+
<https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>}.
|
|
9
|
+
|
|
10
|
+
Class L{RhumbLineBase} has been enhanced with methods C{Intersecant2}, C{Intersection} and C{PlumbTo}
|
|
11
|
+
to iteratively find the intersection of a rhumb line and a circle or an other rhumb line, respectively
|
|
12
|
+
a perpendicular geodesic or other rhumb line.
|
|
13
|
+
|
|
14
|
+
For more details, see the C++ U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/index.html>}
|
|
15
|
+
documentation, especially the U{Class List<https://GeographicLib.SourceForge.io/C++/doc/annotated.html>},
|
|
16
|
+
the background information on U{Rhumb lines<https://GeographicLib.SourceForge.io/C++/doc/rhumb.html>},
|
|
17
|
+
the utily U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>} and U{Online
|
|
18
|
+
rhumb line calculations<https://GeographicLib.SourceForge.io/cgi-bin/RhumbSolve>}.
|
|
19
|
+
|
|
20
|
+
Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2014-2023) and licensed under the MIT/X11
|
|
21
|
+
License. For more information, see the U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.
|
|
22
|
+
'''
|
|
23
|
+
# make sure int/int division yields float quotient
|
|
24
|
+
from __future__ import division as _; del _ # PYCHOK semicolon
|
|
25
|
+
|
|
26
|
+
from pygeodesy.basics import _copysign, itemsorted, unsigned0, _xinstanceof
|
|
27
|
+
from pygeodesy.constants import EPS, EPS0, EPS1, INT0, NAN, _over, \
|
|
28
|
+
_EPSqrt as _TOL, _0_0, _0_01, _1_0, _90_0
|
|
29
|
+
from pygeodesy.datums import Datum, _earth_datum, _spherical_datum, _WGS84
|
|
30
|
+
from pygeodesy.errors import IntersectionError, RhumbError, _xdatum, \
|
|
31
|
+
_xkwds, _xkwds_pop2, _Xorder
|
|
32
|
+
# from pygeodesy.etm import ExactTransverseMercator # _MODS
|
|
33
|
+
from pygeodesy.fmath import euclid, favg, sqrt_a, Fsum
|
|
34
|
+
# from pygeodesy.formy import opposing # _MODS
|
|
35
|
+
# from pygeodesy.fsums import Fsum # from .fmath
|
|
36
|
+
from pygeodesy.interns import NN, _coincident_, _COMMASPACE_, _Dash, \
|
|
37
|
+
_dunder_nameof, _parallel_, _too_, _under
|
|
38
|
+
from pygeodesy.karney import _atan2d, Caps, _CapsBase, _diff182, _fix90, \
|
|
39
|
+
_norm180, GDict
|
|
40
|
+
# from pygeodesy.ktm import KTransverseMercator, _AlpCoeffs # _MODS
|
|
41
|
+
from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS
|
|
42
|
+
# from pygeodesy.named import notOverloaded # _MODS
|
|
43
|
+
from pygeodesy.namedTuples import Distance2Tuple, LatLon2Tuple
|
|
44
|
+
from pygeodesy.props import deprecated_method, Property, Property_RO, \
|
|
45
|
+
property_RO, _update_all
|
|
46
|
+
from pygeodesy.streprs import Fmt, pairs
|
|
47
|
+
from pygeodesy.units import Float_, Lat, Lon, Meter, Radius_, Int # PYCHOK shared
|
|
48
|
+
from pygeodesy.utily import acos1, _azireversed, _loneg, sincos2d, sincos2d_, \
|
|
49
|
+
_unrollon, _Wrap
|
|
50
|
+
from pygeodesy.vector3d import _intersect3d3, Vector3d # in .Intersection below
|
|
51
|
+
|
|
52
|
+
from math import cos, fabs
|
|
53
|
+
|
|
54
|
+
__all__ = ()
|
|
55
|
+
__version__ = '24.03.16'
|
|
56
|
+
|
|
57
|
+
_anti_ = _Dash('anti')
|
|
58
|
+
_rls = [] # instances of C{RbumbLine...} to be updated
|
|
59
|
+
_TRIPS = 65 # .Intersection, .PlumbTo, 19+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _Lat(Lat):
|
|
63
|
+
'''(INTERNAL) Latitude B{C{lat}}.
|
|
64
|
+
'''
|
|
65
|
+
def __init__(self, *lat, **Error_name):
|
|
66
|
+
kwds = _xkwds(Error_name, clip=0, Error=RhumbError)
|
|
67
|
+
Lat.__new__(_Lat, *lat, **kwds)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _Lon(Lon):
|
|
71
|
+
'''(INTERNAL) Longitude B{C{lon}}.
|
|
72
|
+
'''
|
|
73
|
+
def __init__(self, *lon, **Error_name):
|
|
74
|
+
kwds = _xkwds(Error_name, clip=0, Error=RhumbError)
|
|
75
|
+
Lon.__new__(_Lon, *lon, **kwds)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _update_all_rls(r):
|
|
79
|
+
'''(INTERNAL) Zap cached/memoized C{Property[_RO]}s
|
|
80
|
+
of any C{RhumbLine} instances tied to the given
|
|
81
|
+
C{Rhumb} instance B{C{r}}.
|
|
82
|
+
'''
|
|
83
|
+
# _xinstanceof(_MODS.rhumb.aux_.RhumbAux, _MODS.rhumb.ekx.Rhumb, r=r)
|
|
84
|
+
_update_all(r)
|
|
85
|
+
for rl in _rls: # PYCHOK use weakref?
|
|
86
|
+
if rl._rhumb is r:
|
|
87
|
+
_update_all(rl)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class RhumbBase(_CapsBase):
|
|
91
|
+
'''(INTERNAL) Base class for C{rhumb.aux_.RhumbAux} and C{rhumb.ekx.Rhumb}.
|
|
92
|
+
'''
|
|
93
|
+
_datum = _WGS84
|
|
94
|
+
_exact = True
|
|
95
|
+
_f_max = _0_01
|
|
96
|
+
_mTM = 6 # see .TMorder
|
|
97
|
+
|
|
98
|
+
def __init__(self, a_earth, f, exact, name):
|
|
99
|
+
'''New C{RhumbAux} or C{Rhumb}.
|
|
100
|
+
'''
|
|
101
|
+
_earth_datum(self, a_earth, f=f, name=name)
|
|
102
|
+
if not exact:
|
|
103
|
+
self.exact = False
|
|
104
|
+
if name:
|
|
105
|
+
self.name = name
|
|
106
|
+
|
|
107
|
+
@Property_RO
|
|
108
|
+
def a(self):
|
|
109
|
+
'''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}).
|
|
110
|
+
'''
|
|
111
|
+
return self.ellipsoid.a
|
|
112
|
+
|
|
113
|
+
equatoradius = a
|
|
114
|
+
|
|
115
|
+
def ArcDirect(self, lat1, lon1, azi12, a12, outmask=Caps.LATITUDE_LONGITUDE):
|
|
116
|
+
'''Solve the I{direct rhumb} problem, optionally with area.
|
|
117
|
+
|
|
118
|
+
@arg lat1: Latitude of the first point (C{degrees90}).
|
|
119
|
+
@arg lon1: Longitude of the first point (C{degrees180}).
|
|
120
|
+
@arg azi12: Azimuth of the rhumb line (compass C{degrees}).
|
|
121
|
+
@arg a12: Angle along the rhumb line from the given to the
|
|
122
|
+
destination point (C{degrees}), can be negative.
|
|
123
|
+
|
|
124
|
+
@return: L{GDict} with 2 up to 8 items C{lat2, lon2, a12, S12,
|
|
125
|
+
lat1, lon1, azi12, s12} with the destination point's
|
|
126
|
+
latitude C{lat2} and longitude C{lon2} in C{degrees},
|
|
127
|
+
the rhumb angle C{a12} in C{degrees} and area C{S12}
|
|
128
|
+
under the rhumb line in C{meter} I{squared}.
|
|
129
|
+
|
|
130
|
+
@raise ImportError: Package C{numpy} not found or not installed,
|
|
131
|
+
only required for area C{S12} when C{B{exact}
|
|
132
|
+
is True} and L{RhumbAux}.
|
|
133
|
+
|
|
134
|
+
@note: If B{C{a12}} is large enough that the rhumb line crosses
|
|
135
|
+
a pole, the longitude of the second point is indeterminate
|
|
136
|
+
and C{NAN} is returned for C{lon2} and area C{S12}.
|
|
137
|
+
|
|
138
|
+
@note: If the given point is a pole, the cosine of its latitude is
|
|
139
|
+
taken to be C{sqrt(L{EPS})}. This position is extremely
|
|
140
|
+
close to the actual pole and allows the calculation to be
|
|
141
|
+
carried out in finite terms.
|
|
142
|
+
'''
|
|
143
|
+
s12 = a12 * self._mpd
|
|
144
|
+
return self._DirectRhumb(lat1, lon1, azi12, a12, s12, outmask)
|
|
145
|
+
|
|
146
|
+
@Property_RO
|
|
147
|
+
def b(self):
|
|
148
|
+
'''Get the C{ellipsoid}'s polar radius, semi-axis (C{meter}).
|
|
149
|
+
'''
|
|
150
|
+
return self.ellipsoid.b
|
|
151
|
+
|
|
152
|
+
polaradius = b
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def datum(self):
|
|
156
|
+
'''Get this rhumb's datum (L{Datum}).
|
|
157
|
+
'''
|
|
158
|
+
return self._datum
|
|
159
|
+
|
|
160
|
+
@datum.setter # PYCHOK setter!
|
|
161
|
+
def datum(self, datum):
|
|
162
|
+
'''Set this rhumb's datum (L{Datum}).
|
|
163
|
+
|
|
164
|
+
@raise RhumbError: If C{abs(B{f}} exceeds non-zero C{f_max} and C{exact=False}.
|
|
165
|
+
'''
|
|
166
|
+
_xinstanceof(Datum, datum=datum)
|
|
167
|
+
if self._datum != datum:
|
|
168
|
+
self._exactest(self.exact, datum.ellipsoid, self.f_max)
|
|
169
|
+
_update_all_rls(self)
|
|
170
|
+
self._datum = datum
|
|
171
|
+
|
|
172
|
+
def _Direct(self, ll1, azi12, s12, **outmask):
|
|
173
|
+
'''(INTERNAL) Short-cut version, see .latlonBase.rhumb....
|
|
174
|
+
'''
|
|
175
|
+
return self.Direct(ll1.lat, ll1.lon, azi12, s12, **outmask)
|
|
176
|
+
|
|
177
|
+
def Direct(self, lat1, lon1, azi12, s12, outmask=Caps.LATITUDE_LONGITUDE):
|
|
178
|
+
'''Solve the I{direct rhumb} problem, optionally with area.
|
|
179
|
+
|
|
180
|
+
@arg lat1: Latitude of the first point (C{degrees90}).
|
|
181
|
+
@arg lon1: Longitude of the first point (C{degrees180}).
|
|
182
|
+
@arg azi12: Azimuth of the rhumb line (compass C{degrees}).
|
|
183
|
+
@arg s12: Distance along the rhumb line from the given to
|
|
184
|
+
the destination point (C{meter}), can be negative.
|
|
185
|
+
|
|
186
|
+
@return: L{GDict} with 2 up to 8 items C{lat2, lon2, a12, S12,
|
|
187
|
+
lat1, lon1, azi12, s12} with the destination point's
|
|
188
|
+
latitude C{lat2} and longitude C{lon2} in C{degrees},
|
|
189
|
+
the rhumb angle C{a12} in C{degrees} and area C{S12}
|
|
190
|
+
under the rhumb line in C{meter} I{squared}.
|
|
191
|
+
|
|
192
|
+
@raise ImportError: Package C{numpy} not found or not installed,
|
|
193
|
+
only required for area C{S12} when C{B{exact}
|
|
194
|
+
is True} and L{RhumbAux}.
|
|
195
|
+
|
|
196
|
+
@note: If B{C{s12}} is large enough that the rhumb line crosses
|
|
197
|
+
a pole, the longitude of the second point is indeterminate
|
|
198
|
+
and C{NAN} is returned for C{lon2} and area C{S12}.
|
|
199
|
+
|
|
200
|
+
@note: If the given point is a pole, the cosine of its latitude is
|
|
201
|
+
taken to be C{sqrt(L{EPS})}. This position is extremely
|
|
202
|
+
close to the actual pole and allows the calculation to be
|
|
203
|
+
carried out in finite terms.
|
|
204
|
+
'''
|
|
205
|
+
a12 = _over(s12, self._mpd)
|
|
206
|
+
return self._DirectRhumb(lat1, lon1, azi12, a12, s12, outmask)
|
|
207
|
+
|
|
208
|
+
def Direct8(self, lat1, lon1, azi12, s12, outmask=Caps.LATITUDE_LONGITUDE_AREA):
|
|
209
|
+
'''Like method L{Rhumb.Direct} but returning a L{Rhumb8Tuple} with area C{S12}.
|
|
210
|
+
'''
|
|
211
|
+
return self.Direct(lat1, lon1, azi12, s12, outmask=outmask).toRhumb8Tuple()
|
|
212
|
+
|
|
213
|
+
def _DirectLine(self, ll1, azi12, **caps_name):
|
|
214
|
+
'''(INTERNAL) Short-cut version, see .latlonBase.
|
|
215
|
+
'''
|
|
216
|
+
return self.DirectLine(ll1.lat, ll1.lon, azi12, **caps_name)
|
|
217
|
+
|
|
218
|
+
def DirectLine(self, lat1, lon1, azi12, **caps_name):
|
|
219
|
+
'''Define a C{RhumbLine} in terms of the I{direct} rhumb
|
|
220
|
+
problem to compute several points on a single rhumb line.
|
|
221
|
+
|
|
222
|
+
@arg lat1: Latitude of the first point (C{degrees90}).
|
|
223
|
+
@arg lon1: Longitude of the first point (C{degrees180}).
|
|
224
|
+
@arg azi12: Azimuth of the rhumb line (compass C{degrees}).
|
|
225
|
+
@kwarg caps_name: Optional keyword arguments C{B{name}=NN} and
|
|
226
|
+
C{B{caps}=Caps.STANDARD}, a bit-or'ed combination of
|
|
227
|
+
L{Caps} values specifying the required capabilities.
|
|
228
|
+
Include C{Caps.LINE_OFF} if updates to the B{C{rhumb}}
|
|
229
|
+
should I{not} be reflected in this rhumb line.
|
|
230
|
+
|
|
231
|
+
@return: A C{RhumbLine...} instance and invoke its method
|
|
232
|
+
C{.Position} to compute each point.
|
|
233
|
+
|
|
234
|
+
@note: Updates to this rhumb are reflected in the returned
|
|
235
|
+
rhumb line, unless C{B{caps} |= Caps.LINE_OFF}.
|
|
236
|
+
'''
|
|
237
|
+
return self._RhumbLine(self, lat1, lon1, azi12, **caps_name)
|
|
238
|
+
|
|
239
|
+
Line = DirectLine # synonyms
|
|
240
|
+
|
|
241
|
+
def _DirectRhumb(self, lat1, lon1, azi12, a12, s12, outmask):
|
|
242
|
+
'''(INTERNAL) See methods C{.ArcDirect} and C{.Direct}.
|
|
243
|
+
'''
|
|
244
|
+
rl = self._RhumbLine(self, lat1, lon1, azi12, caps=Caps.LINE_OFF,
|
|
245
|
+
name=self.name)
|
|
246
|
+
return rl._Position(a12, s12, outmask | self._debug) # lat2, lon2, S12
|
|
247
|
+
|
|
248
|
+
@Property
|
|
249
|
+
def ellipsoid(self):
|
|
250
|
+
'''Get this rhumb's ellipsoid (L{Ellipsoid}).
|
|
251
|
+
'''
|
|
252
|
+
return self.datum.ellipsoid
|
|
253
|
+
|
|
254
|
+
@ellipsoid.setter # PYCHOK setter!
|
|
255
|
+
def ellipsoid(self, a_earth_f):
|
|
256
|
+
'''Set this rhumb's ellipsoid (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or
|
|
257
|
+
L{a_f2Tuple}) or (equatorial) radius and flattening (2-tuple C{(a, f)}).
|
|
258
|
+
|
|
259
|
+
@raise RhumbError: If C{abs(B{f}} exceeds non-zero C{f_max} and C{exact=False}.
|
|
260
|
+
'''
|
|
261
|
+
self.datum = _spherical_datum(a_earth_f, Error=RhumbError)
|
|
262
|
+
|
|
263
|
+
@Property
|
|
264
|
+
def exact(self):
|
|
265
|
+
'''Get the I{exact} option (C{bool}).
|
|
266
|
+
'''
|
|
267
|
+
return self._exact
|
|
268
|
+
|
|
269
|
+
@exact.setter # PYCHOK setter!
|
|
270
|
+
def exact(self, exact):
|
|
271
|
+
'''Set the I{exact} option (C{bool}). If C{True}, use I{exact} rhumb
|
|
272
|
+
expressions, otherwise a series expansion (accurate for oblate or
|
|
273
|
+
prolate ellipsoids with C{abs(flattening)} below C{f_max}.
|
|
274
|
+
|
|
275
|
+
@raise RhumbError: If C{B{exact}=False} and C{abs(flattening})
|
|
276
|
+
exceeds non-zero C{f_max}.
|
|
277
|
+
|
|
278
|
+
@see: Option U{B{-s}<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>}
|
|
279
|
+
and U{ACCURACY<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html#ACCURACY>}.
|
|
280
|
+
'''
|
|
281
|
+
x = bool(exact)
|
|
282
|
+
if self._exact != x:
|
|
283
|
+
self._exactest(x, self.ellipsoid, self.f_max)
|
|
284
|
+
_update_all_rls(self)
|
|
285
|
+
self._exact = x
|
|
286
|
+
|
|
287
|
+
def _exactest(self, exact, ellipsoid, f_max):
|
|
288
|
+
# Helper for property setters C{ellipsoid}, C{exact} and C{f_max}
|
|
289
|
+
if fabs(ellipsoid.f) > f_max > 0 and not exact:
|
|
290
|
+
raise RhumbError(exact=exact, f=ellipsoid.f, f_max=f_max)
|
|
291
|
+
|
|
292
|
+
@Property_RO
|
|
293
|
+
def f(self):
|
|
294
|
+
'''Get the C{ellipsoid}'s flattening (C{float}).
|
|
295
|
+
'''
|
|
296
|
+
return self.ellipsoid.f
|
|
297
|
+
|
|
298
|
+
flattening = f
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def f_max(self):
|
|
302
|
+
'''Get the I{max.} flattening (C{float}).
|
|
303
|
+
'''
|
|
304
|
+
return self._f_max
|
|
305
|
+
|
|
306
|
+
@f_max.setter # PYCHOK setter!
|
|
307
|
+
def f_max(self, f_max): # PYCHOK no cover
|
|
308
|
+
'''Set the I{max.} flattening, not to exceed (C{float}).
|
|
309
|
+
|
|
310
|
+
@raise RhumbError: If C{exact=False} and C{abs(flattening})
|
|
311
|
+
exceeds non-zero C{f_max}.
|
|
312
|
+
'''
|
|
313
|
+
f = Float_(f_max=f_max, low=_0_0, high=EPS1)
|
|
314
|
+
if self._f_max != f:
|
|
315
|
+
self._exactest(self.exact, self.ellipsoid, f)
|
|
316
|
+
self._f_max = f
|
|
317
|
+
|
|
318
|
+
def _Inverse(self, ll1, ll2, wrap, **outmask):
|
|
319
|
+
'''(INTERNAL) Short-cut version, see .latlonBase.rhumb....
|
|
320
|
+
'''
|
|
321
|
+
if wrap:
|
|
322
|
+
ll2 = _unrollon(ll1, _Wrap.point(ll2))
|
|
323
|
+
return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask)
|
|
324
|
+
|
|
325
|
+
def Inverse(self, lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH_DISTANCE):
|
|
326
|
+
'''Solve the I{inverse rhumb} problem.
|
|
327
|
+
|
|
328
|
+
@arg lat1: Latitude of the first point (C{degrees90}).
|
|
329
|
+
@arg lon1: Longitude of the first point (C{degrees180}).
|
|
330
|
+
@arg lat2: Latitude of the second point (C{degrees90}).
|
|
331
|
+
@arg lon2: Longitude of the second point (C{degrees180}).
|
|
332
|
+
|
|
333
|
+
@return: L{GDict} with 4 to 9 items C{lat1, lon1, lat2, lon2,
|
|
334
|
+
azi12, azi21, s12, a12, S12}, the rhumb line's azimuth
|
|
335
|
+
C{azi12} and I{reverse} azimuth C{azi21}, both in
|
|
336
|
+
compass C{degrees} between C{-180} and C{+180}, the
|
|
337
|
+
rhumb distance C{s12} and rhumb angle C{a12} between
|
|
338
|
+
both points in C{meter} respectively C{degrees} and
|
|
339
|
+
the area C{S12} under the rhumb line in C{meter}
|
|
340
|
+
I{squared}.
|
|
341
|
+
|
|
342
|
+
@raise ImportError: Package C{numpy} not found or not installed,
|
|
343
|
+
only required for L{RhumbAux} area C{S12}
|
|
344
|
+
when C{B{exact} is True}.
|
|
345
|
+
|
|
346
|
+
@note: The shortest rhumb line is found. If the end points are
|
|
347
|
+
on opposite meridians, there are two shortest rhumb lines
|
|
348
|
+
and the East-going one is chosen.
|
|
349
|
+
|
|
350
|
+
@note: If either point is a pole, the cosine of its latitude is
|
|
351
|
+
taken to be C{sqrt(L{EPS})}. This position is extremely
|
|
352
|
+
close to the actual pole and allows the calculation to be
|
|
353
|
+
carried out in finite terms.
|
|
354
|
+
'''
|
|
355
|
+
r = GDict(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2, name=self.name)
|
|
356
|
+
Cs = Caps
|
|
357
|
+
if (outmask & Cs.AZIMUTH_DISTANCE_AREA):
|
|
358
|
+
lon12, _ = _diff182(lon1, lon2, K_2_0=True)
|
|
359
|
+
y, x, s1, s2 = self._Inverse4(lon12, r, outmask)
|
|
360
|
+
if (outmask & Cs.AZIMUTH):
|
|
361
|
+
z = _atan2d(y, x)
|
|
362
|
+
r.set_(azi12=z, azi21=_azireversed(z))
|
|
363
|
+
if (outmask & Cs.AREA):
|
|
364
|
+
S12 = self._S12d(s1, s2, lon12)
|
|
365
|
+
r.set_(S12=unsigned0(S12)) # like .gx
|
|
366
|
+
return r
|
|
367
|
+
|
|
368
|
+
def _Inverse4(self, lon12, r, outmask): # PYCHOK no cover
|
|
369
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
370
|
+
_MODS.named.notOverloaded(self, lon12, r, Caps.toStr(outmask))
|
|
371
|
+
|
|
372
|
+
def Inverse8(self, lat1, lon1, azi12, s12, outmask=Caps.AZIMUTH_DISTANCE_AREA):
|
|
373
|
+
'''Like method L{Rhumb.Inverse} but returning a L{Rhumb8Tuple} with area C{S12}.
|
|
374
|
+
'''
|
|
375
|
+
return self.Inverse(lat1, lon1, azi12, s12, outmask=outmask).toRhumb8Tuple()
|
|
376
|
+
|
|
377
|
+
def _InverseLine(self, ll1, ll2, wrap, **caps_name):
|
|
378
|
+
'''(INTERNAL) Short-cut version, see .latlonBase.
|
|
379
|
+
'''
|
|
380
|
+
if wrap:
|
|
381
|
+
ll2 = _unrollon(ll1, _Wrap.point(ll2))
|
|
382
|
+
return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name)
|
|
383
|
+
|
|
384
|
+
def InverseLine(self, lat1, lon1, lat2, lon2, **caps_name):
|
|
385
|
+
'''Define a C{RhumbLine} in terms of the I{inverse} rhumb problem.
|
|
386
|
+
|
|
387
|
+
@arg lat1: Latitude of the first point (C{degrees90}).
|
|
388
|
+
@arg lon1: Longitude of the first point (C{degrees180}).
|
|
389
|
+
@arg lat2: Latitude of the second point (C{degrees90}).
|
|
390
|
+
@arg lon2: Longitude of the second point (C{degrees180}).
|
|
391
|
+
@kwarg caps_name: Optional keyword arguments C{B{name}=NN} and
|
|
392
|
+
C{B{caps}=Caps.STANDARD}, a bit-or'ed combination of
|
|
393
|
+
L{Caps} values specifying the required capabilities.
|
|
394
|
+
Include C{Caps.LINE_OFF} if updates to the B{C{rhumb}}
|
|
395
|
+
should I{not} be reflected in this rhumb line.
|
|
396
|
+
|
|
397
|
+
@return: A C{RhumbLine...} instance and invoke its method
|
|
398
|
+
C{ArcPosition} or C{Position} to compute points.
|
|
399
|
+
|
|
400
|
+
@note: Updates to this rhumb are reflected in the returned
|
|
401
|
+
rhumb line, unless C{B{caps} |= Caps.LINE_OFF}.
|
|
402
|
+
'''
|
|
403
|
+
r = self.Inverse(lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH)
|
|
404
|
+
return self._RhumbLine(self, lat1, lon1, r.azi12, **caps_name)
|
|
405
|
+
|
|
406
|
+
@Property_RO
|
|
407
|
+
def _mpd(self): # PYCHOK no cover
|
|
408
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
409
|
+
_MODS.named.notOverloaded(self)
|
|
410
|
+
|
|
411
|
+
@property_RO
|
|
412
|
+
def RAorder(self):
|
|
413
|
+
'''Get the I{Rhumb Area} order, C{None} always.
|
|
414
|
+
'''
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
@property_RO
|
|
418
|
+
def _RhumbLine(self): # PYCHOK no cover
|
|
419
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
420
|
+
_MODS.named.notOverloaded(self, underOK=True)
|
|
421
|
+
|
|
422
|
+
def _S12d(self, s1, s2, lon): # PYCHOK no cover
|
|
423
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
424
|
+
_MODS.named.notOverloaded(self, s1, s2, lon)
|
|
425
|
+
|
|
426
|
+
@Property
|
|
427
|
+
def TMorder(self):
|
|
428
|
+
'''Get the I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).
|
|
429
|
+
'''
|
|
430
|
+
return self._mTM
|
|
431
|
+
|
|
432
|
+
@TMorder.setter # PYCHOK setter!
|
|
433
|
+
def TMorder(self, order):
|
|
434
|
+
'''Set the I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).
|
|
435
|
+
|
|
436
|
+
@note: Setting C{TMorder} turns property C{exact} off, but only
|
|
437
|
+
for L{Rhumb} instances.
|
|
438
|
+
'''
|
|
439
|
+
m = _Xorder(_MODS.ktm._AlpCoeffs, RhumbError, TMorder=order)
|
|
440
|
+
if self._mTM != m:
|
|
441
|
+
_update_all_rls(self)
|
|
442
|
+
self._mTM = m
|
|
443
|
+
if self.exact and isinstance(self, _MODS.rhumb.ekx.Rhumb):
|
|
444
|
+
self.exact = False
|
|
445
|
+
|
|
446
|
+
def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
|
|
447
|
+
'''Return this C{Rhumb} as string.
|
|
448
|
+
|
|
449
|
+
@kwarg prec: The C{float} precision, number of decimal digits (0..9).
|
|
450
|
+
Trailing zero decimals are stripped for B{C{prec}} values
|
|
451
|
+
of 1 and above, but kept for negative B{C{prec}} values.
|
|
452
|
+
@kwarg sep: Separator to join (C{str}).
|
|
453
|
+
|
|
454
|
+
@return: Tuple items (C{str}).
|
|
455
|
+
'''
|
|
456
|
+
d = dict(ellipsoid=self.ellipsoid, RAorder=self.RAorder,
|
|
457
|
+
exact=self.exact, TMorder=self.TMorder)
|
|
458
|
+
return sep.join(pairs(itemsorted(d, asorted=False), prec=prec))
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class RhumbLineBase(_CapsBase):
|
|
462
|
+
'''(INTERNAL) Base class for C{rhumb.aux_.RhumbLineAux} and C{rhumb.ekx.RhumbLine}.
|
|
463
|
+
'''
|
|
464
|
+
_azi12 = _0_0
|
|
465
|
+
_calp = _1_0
|
|
466
|
+
# _caps = \
|
|
467
|
+
# _debug = 0
|
|
468
|
+
# _lat1 = \
|
|
469
|
+
# _lon1 = \
|
|
470
|
+
# _lon12 = _0_0
|
|
471
|
+
_Rhumb = RhumbBase # compatible C{Rhumb} class
|
|
472
|
+
_rhumb = None # C{Rhumb} instance
|
|
473
|
+
_salp = \
|
|
474
|
+
_talp = _0_0
|
|
475
|
+
|
|
476
|
+
def __init__(self, rhumb, lat1, lon1, azi12, caps=Caps.STANDARD, name=NN):
|
|
477
|
+
'''New C{RhumbLine} or C{RhumbLineAux}.
|
|
478
|
+
'''
|
|
479
|
+
_xinstanceof(self._Rhumb, rhumb=rhumb)
|
|
480
|
+
|
|
481
|
+
self._lat1 = _Lat(lat1=_fix90(lat1))
|
|
482
|
+
self._lon1 = _Lon(lon1= lon1)
|
|
483
|
+
self._lon12 = _norm180(self._lon1)
|
|
484
|
+
if azi12: # non-zero, non-None
|
|
485
|
+
self.azi12 = _norm180(azi12)
|
|
486
|
+
|
|
487
|
+
n = name or rhumb.name
|
|
488
|
+
if n:
|
|
489
|
+
self.name=n
|
|
490
|
+
|
|
491
|
+
self._caps = caps
|
|
492
|
+
self._debug |= (caps | rhumb._debug) & Caps._DEBUG_DIRECT_LINE
|
|
493
|
+
if (caps & Caps.LINE_OFF): # copy to avoid updates
|
|
494
|
+
self._rhumb = rhumb.copy(deep=False, name=_under(rhumb.name))
|
|
495
|
+
else:
|
|
496
|
+
self._rhumb = rhumb
|
|
497
|
+
_rls.append(self)
|
|
498
|
+
|
|
499
|
+
def __del__(self): # XXX use weakref?
|
|
500
|
+
if _rls: # may be empty or None
|
|
501
|
+
try: # PYCHOK no cover
|
|
502
|
+
_rls.remove(self)
|
|
503
|
+
except (TypeError, ValueError):
|
|
504
|
+
pass
|
|
505
|
+
self._rhumb = None
|
|
506
|
+
# _update_all(self) # throws TypeError during Python 2 cleanup
|
|
507
|
+
|
|
508
|
+
def ArcPosition(self, a12, outmask=Caps.LATITUDE_LONGITUDE):
|
|
509
|
+
'''Compute a point at a given angular distance on this rhumb line.
|
|
510
|
+
|
|
511
|
+
@arg a12: The angle along this rhumb line from its origin to the
|
|
512
|
+
point (C{degrees}), can be negative.
|
|
513
|
+
@kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
|
|
514
|
+
the quantities to be returned.
|
|
515
|
+
|
|
516
|
+
@return: L{GDict} with 4 to 8 items C{azi12, a12, s12, S12, lat2,
|
|
517
|
+
lon2, lat1, lon1} with latitude C{lat2} and longitude
|
|
518
|
+
C{lon2} of the point in C{degrees}, the rhumb distance
|
|
519
|
+
C{s12} in C{meter} from the start point of and the area
|
|
520
|
+
C{S12} under this rhumb line in C{meter} I{squared}.
|
|
521
|
+
|
|
522
|
+
@raise ImportError: Package C{numpy} not found or not installed,
|
|
523
|
+
only required for L{RhumbLineAux} area C{S12}
|
|
524
|
+
when C{B{exact} is True}.
|
|
525
|
+
|
|
526
|
+
@note: If B{C{a12}} is large enough that the rhumb line crosses a
|
|
527
|
+
pole, the longitude of the second point is indeterminate and
|
|
528
|
+
C{NAN} is returned for C{lon2} and area C{S12}.
|
|
529
|
+
|
|
530
|
+
If the first point is a pole, the cosine of its latitude is
|
|
531
|
+
taken to be C{sqrt(L{EPS})}. This position is extremely
|
|
532
|
+
close to the actual pole and allows the calculation to be
|
|
533
|
+
carried out in finite terms.
|
|
534
|
+
'''
|
|
535
|
+
return self._Position(a12, self.degrees2m(a12), outmask)
|
|
536
|
+
|
|
537
|
+
@Property
|
|
538
|
+
def azi12(self):
|
|
539
|
+
'''Get this rhumb line's I{azimuth} (compass C{degrees}).
|
|
540
|
+
'''
|
|
541
|
+
return self._azi12
|
|
542
|
+
|
|
543
|
+
@azi12.setter # PYCHOK setter!
|
|
544
|
+
def azi12(self, azi12):
|
|
545
|
+
'''Set this rhumb line's I{azimuth} (compass C{degrees}).
|
|
546
|
+
'''
|
|
547
|
+
z = _norm180(azi12)
|
|
548
|
+
if self._azi12 != z:
|
|
549
|
+
if self._rhumb:
|
|
550
|
+
_update_all(self)
|
|
551
|
+
self._azi12 = z
|
|
552
|
+
self._salp, self._calp = t = sincos2d(z) # no NEG0
|
|
553
|
+
self._talp = _over(*t)
|
|
554
|
+
|
|
555
|
+
@property_RO
|
|
556
|
+
def azi12_sincos2(self): # PYCHOK no cover
|
|
557
|
+
'''Get the sine and cosine of this rhumb line's I{azimuth} (2-tuple C{(sin, cos)}).
|
|
558
|
+
'''
|
|
559
|
+
return self._scalp, self._calp
|
|
560
|
+
|
|
561
|
+
@property_RO
|
|
562
|
+
def datum(self):
|
|
563
|
+
'''Get this rhumb line's datum (L{Datum}).
|
|
564
|
+
'''
|
|
565
|
+
return self.rhumb.datum
|
|
566
|
+
|
|
567
|
+
def degrees2m(self, angle):
|
|
568
|
+
'''Convert an angular distance along this rhumb line to C{meter}.
|
|
569
|
+
|
|
570
|
+
@arg angle: Angular distance (C{degrees}).
|
|
571
|
+
|
|
572
|
+
@return: Distance (C{meter}).
|
|
573
|
+
'''
|
|
574
|
+
return float(angle) * self.rhumb._mpd
|
|
575
|
+
|
|
576
|
+
@deprecated_method
|
|
577
|
+
def distance2(self, lat, lon): # PYCHOK no cover
|
|
578
|
+
'''DEPRECATED on 23.09.23, use method L{RhumbLineAux.Inverse} or L{RhumbLine.Inverse}.
|
|
579
|
+
|
|
580
|
+
@return: A L{Distance2Tuple}C{(distance, initial)} with the C{distance}
|
|
581
|
+
in C{meter} and C{initial} bearing (azimuth) in C{degrees}.
|
|
582
|
+
'''
|
|
583
|
+
r = self.Inverse(lat, lon)
|
|
584
|
+
return Distance2Tuple(r.s12, r.azi12)
|
|
585
|
+
|
|
586
|
+
@property_RO
|
|
587
|
+
def ellipsoid(self):
|
|
588
|
+
'''Get this rhumb line's ellipsoid (L{Ellipsoid}).
|
|
589
|
+
'''
|
|
590
|
+
return self.rhumb.ellipsoid
|
|
591
|
+
|
|
592
|
+
@property_RO
|
|
593
|
+
def exact(self):
|
|
594
|
+
'''Get this rhumb line's I{exact} option (C{bool}).
|
|
595
|
+
'''
|
|
596
|
+
return self.rhumb.exact
|
|
597
|
+
|
|
598
|
+
def Intersecant2(self, lat0, lon0, radius, napier=True, **tol_eps):
|
|
599
|
+
'''Compute the intersection(s) of this rhumb line and a circle.
|
|
600
|
+
|
|
601
|
+
@arg lat0: Latitude of the circle center (C{degrees}).
|
|
602
|
+
@arg lon0: Longitude of the circle center (C{degrees}).
|
|
603
|
+
@arg radius: Radius of the circle (C{meter}, conventionally).
|
|
604
|
+
@kwarg napier: If C{True}, apply I{Napier}'s spherical triangle
|
|
605
|
+
instead of planar trigonometry (C{bool}).
|
|
606
|
+
@kwarg tol_eps: Optional keyword arguments, see method
|
|
607
|
+
method L{Intersection} for further details.
|
|
608
|
+
|
|
609
|
+
@return: 2-Tuple C{(P, Q)} with both intersections (representing
|
|
610
|
+
a rhumb chord), each a L{GDict} from method L{Intersection}
|
|
611
|
+
extended to 18 items by C{lat3, lon3, azi03, a03, s03}
|
|
612
|
+
with azimuth C{azi03} of, distance C{a03} in C{degrees}
|
|
613
|
+
and C{s03} in C{meter} along the rhumb line from the circle
|
|
614
|
+
C{lat0, lon0} to the chord center C{lat3, lon3}. If this
|
|
615
|
+
rhumb line is tangential to the circle, both points
|
|
616
|
+
are the same L{GDict} instance with distances C{s02} and
|
|
617
|
+
C{s03} near-equal to the B{C{radius}}.
|
|
618
|
+
|
|
619
|
+
@raise IntersectionError: The circle and this rhumb line
|
|
620
|
+
do not intersect.
|
|
621
|
+
|
|
622
|
+
@raise UnitError: Invalid B{C{radius}}.
|
|
623
|
+
'''
|
|
624
|
+
r = Radius_(radius)
|
|
625
|
+
p = q = self.PlumbTo(lat0, lon0, exact=None, **tol_eps)
|
|
626
|
+
a = q.s02
|
|
627
|
+
t = dict(lat3=q.lat2, lon3=q.lon2, azi03=q.azi02, a03=q.a02, s03=a)
|
|
628
|
+
if a < r:
|
|
629
|
+
t.update(iteration=q.iteration, lat0=q.lat1, lon0=q.lon1, # or lat0, lon0
|
|
630
|
+
name=_dunder_nameof(self.Intersecant2, self.name))
|
|
631
|
+
if fabs(a) < EPS0: # coincident centers
|
|
632
|
+
d, h = _0_0, r
|
|
633
|
+
else:
|
|
634
|
+
d = q.s12
|
|
635
|
+
if napier: # Napier rule (R1) cos(b) = cos(c) / cos(a)
|
|
636
|
+
# <https://WikiPedia.org/wiki/Spherical_trigonometry>
|
|
637
|
+
m = self.rhumb._mpr
|
|
638
|
+
h = (acos1(cos(r / m) / cos(a / m)) * m) if m else _0_0
|
|
639
|
+
else:
|
|
640
|
+
h = _copysign(sqrt_a(r, a), a)
|
|
641
|
+
p = q = self.Position(d + h).set_(**t)
|
|
642
|
+
if h:
|
|
643
|
+
q = self.Position(d - h).set_(**t)
|
|
644
|
+
elif a > r:
|
|
645
|
+
t = _too_(Fmt.distant(a))
|
|
646
|
+
raise IntersectionError(self, lat0, lon0, radius,
|
|
647
|
+
txt=t, **tol_eps)
|
|
648
|
+
else: # tangential
|
|
649
|
+
q.set_(**t) # == p.set(_**t)
|
|
650
|
+
return p, q
|
|
651
|
+
|
|
652
|
+
@deprecated_method
|
|
653
|
+
def intersection2(self, other, **tol_eps): # PYCHOK no cover
|
|
654
|
+
'''DEPRECATED on 23.10.10, use method L{Intersection}.'''
|
|
655
|
+
p = self.Intersection(other, **tol_eps)
|
|
656
|
+
r = LatLon2Tuple(p.lat2, p.lon2, name=self.intersection2.__name__)
|
|
657
|
+
r._iteration = p.iteration
|
|
658
|
+
return r
|
|
659
|
+
|
|
660
|
+
def Intersection(self, other, tol=_TOL, **eps):
|
|
661
|
+
'''I{Iteratively} find the intersection of this and an other rhumb line.
|
|
662
|
+
|
|
663
|
+
@arg other: The other rhumb line (C{RhumbLine}).
|
|
664
|
+
@kwarg tol: Tolerance for longitudinal convergence and parallel
|
|
665
|
+
error (C{degrees}).
|
|
666
|
+
@kwarg eps: Tolerance for L{pygeodesy.intersection3d3} (C{EPS}).
|
|
667
|
+
|
|
668
|
+
@return: The intersection point, a L{Position}-like L{GDict} with
|
|
669
|
+
13 items C{lat1, lon1, azi12, a12, s12, lat2, lon2, lat0,
|
|
670
|
+
lon0, azi02, a02, s02, at} with the rhumb angle C{a02}
|
|
671
|
+
and rhumb distance C{s02} between the start point C{lat0,
|
|
672
|
+
lon0} of the B{C{other}} rhumb line and the intersection
|
|
673
|
+
C{lat2, lon2}, the azimuth C{azi02} of the B{C{other}}
|
|
674
|
+
rhumb line and the angle C{at} between both rhumb lines.
|
|
675
|
+
See method L{Position} for further details.
|
|
676
|
+
|
|
677
|
+
@raise IntersectionError: No convergence for this B{C{tol}} or
|
|
678
|
+
no intersection for an other reason.
|
|
679
|
+
|
|
680
|
+
@see: Methods C{distance2} and C{PlumbTo} and function
|
|
681
|
+
L{pygeodesy.intersection3d3}.
|
|
682
|
+
|
|
683
|
+
@note: Each iteration involves a round trip to this rhumb line's
|
|
684
|
+
L{ExactTransverseMercator} or L{KTransverseMercator}
|
|
685
|
+
projection and function L{pygeodesy.intersection3d3} in
|
|
686
|
+
that domain.
|
|
687
|
+
'''
|
|
688
|
+
_xinstanceof(RhumbLineBase, other=other)
|
|
689
|
+
_xdatum(self.rhumb, other.rhumb, Error=RhumbError)
|
|
690
|
+
try:
|
|
691
|
+
if self.others(other) is self:
|
|
692
|
+
raise ValueError(_coincident_)
|
|
693
|
+
# make invariants and globals locals
|
|
694
|
+
_s_3d, s_az = self._xTM3d, self.azi12
|
|
695
|
+
_o_3d, o_az = other._xTM3d, other.azi12
|
|
696
|
+
p = _MODS.formy.opposing(s_az, o_az, margin=tol)
|
|
697
|
+
if p is not None: # == p in (True, False)
|
|
698
|
+
raise ValueError(_anti_(_parallel_) if p else _parallel_)
|
|
699
|
+
_diff = euclid # approximate length
|
|
700
|
+
_i3d3 = _intersect3d3 # NOT .vector3d.intersection3d3
|
|
701
|
+
_LL2T = LatLon2Tuple
|
|
702
|
+
_xTMr = self.xTM.reverse # ellipsoidal or spherical
|
|
703
|
+
# use halfway point as initial estimate
|
|
704
|
+
p = _LL2T(favg(self.lat1, other.lat1),
|
|
705
|
+
favg(self.lon1, other.lon1))
|
|
706
|
+
for i in range(1, _TRIPS):
|
|
707
|
+
v = _i3d3(_s_3d(p), s_az, # point + bearing
|
|
708
|
+
_o_3d(p), o_az, useZ=False, **eps)[0]
|
|
709
|
+
t = _xTMr(v.x, v.y, lon0=p.lon) # PYCHOK Reverse4Tuple
|
|
710
|
+
d = _diff(t.lon - p.lon, t.lat) # PYCHOK t.lat + p.lat - p.lat
|
|
711
|
+
p = _LL2T(t.lat + p.lat, t.lon) # PYCHOK t.lon + p.lon = lon0
|
|
712
|
+
if d < tol: # 19 trips
|
|
713
|
+
break
|
|
714
|
+
else:
|
|
715
|
+
raise ValueError(Fmt.no_convergence(d, tol))
|
|
716
|
+
|
|
717
|
+
P = GDict(lat1=self.lat1, lat2=p.lat, lat0=other.lat1,
|
|
718
|
+
lon1=self.lon1, lon2=p.lon, lon0=other.lon1,
|
|
719
|
+
name=_dunder_nameof(self.Intersection, self.name))
|
|
720
|
+
r = self.Inverse( p.lat, p.lon, outmask=Caps.DISTANCE)
|
|
721
|
+
t = other.Inverse(p.lat, p.lon, outmask=Caps.DISTANCE)
|
|
722
|
+
P.set_(azi12= self.azi12, a12=r.a12, s12=r.s12,
|
|
723
|
+
azi02=other.azi12, a02=t.a12, s02=t.s12,
|
|
724
|
+
at=other.azi12 - self.azi12, iteration=i)
|
|
725
|
+
except Exception as x:
|
|
726
|
+
raise IntersectionError(self, other, tol=tol,
|
|
727
|
+
eps=eps, cause=x)
|
|
728
|
+
return P
|
|
729
|
+
|
|
730
|
+
def Inverse(self, lat2, lon2, wrap=False, **outmask):
|
|
731
|
+
'''Return the rhumb angle, distance, azimuth, I{reverse} azimuth, etc. of
|
|
732
|
+
a rhumb line between the given point and this rhumb line's start point.
|
|
733
|
+
|
|
734
|
+
@arg lat2: Latitude of the point (C{degrees}).
|
|
735
|
+
@arg lon2: Longitude of the points (C{degrees}).
|
|
736
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{lat2}}
|
|
737
|
+
and B{C{lon2}} (C{bool}).
|
|
738
|
+
|
|
739
|
+
@return: L{GDict} with 8 items C{a12, s12, azi12, azi21, lat1, lon1,
|
|
740
|
+
lat2, lon2}, the rhumb angle C{a12} and rhumb distance C{s12}
|
|
741
|
+
between both points in C{degrees} respectively C{meter}, the
|
|
742
|
+
rhumb line's azimuth C{azi12} and I{reverse} azimuth C{azi21}
|
|
743
|
+
both in compass C{degrees} between C{-180} and C{+180}.
|
|
744
|
+
'''
|
|
745
|
+
if wrap:
|
|
746
|
+
_, lat2, lon2 = _Wrap.latlon3(self.lon1, _fix90(lat2), lon2, wrap)
|
|
747
|
+
r = self.rhumb.Inverse(self.lat1, self.lon1, lat2, lon2, **outmask)
|
|
748
|
+
return r
|
|
749
|
+
|
|
750
|
+
@Property_RO
|
|
751
|
+
def isLoxodrome(self):
|
|
752
|
+
'''Is this rhumb line a meridional (C{None}), a parallel
|
|
753
|
+
(C{False}) or a C{True} loxodrome?
|
|
754
|
+
|
|
755
|
+
@see: I{Osborne's} U{2.5 Rumb lines and loxodromes
|
|
756
|
+
<https://Zenodo.org/record/35392>}, page 37.
|
|
757
|
+
'''
|
|
758
|
+
return bool(self._salp) if self._calp else None
|
|
759
|
+
|
|
760
|
+
@Property_RO
|
|
761
|
+
def lat1(self):
|
|
762
|
+
'''Get this rhumb line's latitude (C{degrees90}).
|
|
763
|
+
'''
|
|
764
|
+
return self._lat1
|
|
765
|
+
|
|
766
|
+
@Property_RO
|
|
767
|
+
def lon1(self):
|
|
768
|
+
'''Get this rhumb line's longitude (C{degrees180}).
|
|
769
|
+
'''
|
|
770
|
+
return self._lon1
|
|
771
|
+
|
|
772
|
+
@Property_RO
|
|
773
|
+
def latlon1(self):
|
|
774
|
+
'''Get this rhumb line's lat- and longitude (L{LatLon2Tuple}C{(lat, lon)}).
|
|
775
|
+
'''
|
|
776
|
+
return LatLon2Tuple(self.lat1, self.lon1)
|
|
777
|
+
|
|
778
|
+
def m2degrees(self, distance):
|
|
779
|
+
'''Convert a distance along this rhumb line to an angular distance.
|
|
780
|
+
|
|
781
|
+
@arg distance: Distance (C{meter}).
|
|
782
|
+
|
|
783
|
+
@return: Angular distance (C{degrees}).
|
|
784
|
+
'''
|
|
785
|
+
return _over(float(distance), self.rhumb._mpd)
|
|
786
|
+
|
|
787
|
+
@property_RO
|
|
788
|
+
def _mu1(self): # PYCHOK no cover
|
|
789
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
790
|
+
_MODS.named.notOverloaded(self, underOK=True)
|
|
791
|
+
|
|
792
|
+
def _mu2lat(self, mu2): # PYCHOK no cover
|
|
793
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
794
|
+
_MODS.named.notOverloaded(self, mu2, underOK=True)
|
|
795
|
+
|
|
796
|
+
@deprecated_method
|
|
797
|
+
def nearestOn4(self, lat0, lon0, **exact_eps_est_tol): # PYCHOK no cover
|
|
798
|
+
'''DEPRECATED on 23.10.10, use method L{PlumbTo}.'''
|
|
799
|
+
P = self.PlumbTo(lat0, lon0, **exact_eps_est_tol)
|
|
800
|
+
r = _MODS.deprecated.classes.NearestOn4Tuple(P.lat2, P.lon2, P.s12, P.azi02,
|
|
801
|
+
name=self.nearestOn4.__name__)
|
|
802
|
+
r._iteration = P.iteration
|
|
803
|
+
return r
|
|
804
|
+
|
|
805
|
+
@deprecated_method
|
|
806
|
+
def NearestOn(self, lat0, lon0, **exact_eps_est_tol): # PYCHOK no cover
|
|
807
|
+
'''DEPRECATED on 23.10.30, use method L{PlumbTo}.'''
|
|
808
|
+
return self.PlumbTo(lat0, lon0, **exact_eps_est_tol)
|
|
809
|
+
|
|
810
|
+
def PlumbTo(self, lat0, lon0, exact=None, eps=EPS, est=None, tol=_TOL):
|
|
811
|
+
'''Compute the I{perpendicular} intersection of this rumb line with a geodesic
|
|
812
|
+
from the given point, in part transcoded from I{Karney}'s C++ U{rhumb-intercept
|
|
813
|
+
<https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>}.
|
|
814
|
+
|
|
815
|
+
@arg lat0: Latitude of the point (C{degrees}).
|
|
816
|
+
@arg lon0: Longitude of the point (C{degrees}).
|
|
817
|
+
@kwarg exact: If C{None}, use a rhumb line perpendicular to this rhumb
|
|
818
|
+
line, otherwise use an I{exact} C{Geodesic...} from the
|
|
819
|
+
given point perpendicular to this rhumb line (C{bool} or
|
|
820
|
+
C{Geodesic...}), see method L{Ellipsoid.geodesic_}.
|
|
821
|
+
@kwarg eps: Optional tolerance for L{pygeodesy.intersection3d3} (C{EPS}),
|
|
822
|
+
used only if C{B{exact} is None}.
|
|
823
|
+
@kwarg est: Optional, initial estimate for the distance C{s12} of the
|
|
824
|
+
intersection I{along} this rhumb line (C{meter}), used only
|
|
825
|
+
if C{B{exact} is not None}.
|
|
826
|
+
@kwarg tol: Longitudinal convergence tolerance (C{degrees}) or distance
|
|
827
|
+
tolerance (C(meter)) when C{B{exact} is None}, respectively
|
|
828
|
+
C{not None}.
|
|
829
|
+
|
|
830
|
+
@return: The intersection point on this rhumb line, a L{GDict} from method
|
|
831
|
+
L{Intersection} if B{C{exact}=None}. If B{C{exact}} is not C{None},
|
|
832
|
+
a L{Position}-like L{GDict} of 13 items C{azi12, a12, s12, lat2,
|
|
833
|
+
lat1, lat0, lon2, lon1, lon0, azi0, a02, s02, at} with distance
|
|
834
|
+
C{a02} in C{degrees} and C{s02} in C{meter} between the given point
|
|
835
|
+
C{lat0, lon0} and the intersection C{lat2, lon2}, geodesic azimuth
|
|
836
|
+
C{azi0} at the given point and the (perpendicular) angle C{at}
|
|
837
|
+
between the geodesic and this rhumb line at the intersection. The
|
|
838
|
+
I{geodesic} azimuth at the intersection is C{(at + azi12)}. See
|
|
839
|
+
method L{Position} for further details.
|
|
840
|
+
|
|
841
|
+
@raise ImportError: I{Karney}'s U{geographiclib
|
|
842
|
+
<https://PyPI.org/project/geographiclib>}
|
|
843
|
+
package not found or not installed.
|
|
844
|
+
|
|
845
|
+
@raise IntersectionError: No convergence for this B{C{eps}} or no
|
|
846
|
+
intersection for some other reason.
|
|
847
|
+
|
|
848
|
+
@see: Methods C{distance2}, C{Intersecant2} and C{Intersection}
|
|
849
|
+
and function L{pygeodesy.intersection3d3}.
|
|
850
|
+
'''
|
|
851
|
+
Cs, tol = Caps, Float_(tol=tol, low=EPS, high=None)
|
|
852
|
+
|
|
853
|
+
# def _over(p, q): # see @note at method C{.Position}
|
|
854
|
+
# if p:
|
|
855
|
+
# p = (p / (q or _copysign(tol, q))) if isfinite(q) else NAN
|
|
856
|
+
# return p
|
|
857
|
+
|
|
858
|
+
if exact is None:
|
|
859
|
+
z = _norm180(self.azi12 + _90_0) # perpendicular azimuth
|
|
860
|
+
rl = RhumbLineBase(self.rhumb, lat0, lon0, z, caps=Cs.LINE_OFF)
|
|
861
|
+
P = self.Intersection(rl, tol=tol, eps=eps)
|
|
862
|
+
|
|
863
|
+
else: # C{rhumb-intercept}
|
|
864
|
+
E = self.ellipsoid
|
|
865
|
+
_gI = E.geodesic_(exact=exact).Inverse
|
|
866
|
+
gm = Cs.STANDARD | Cs._REDUCEDLENGTH_GEODESICSCALE # ^ Cs.DISTANCE_IN
|
|
867
|
+
if est is None: # get an estimate from the "perpendicular" geodesic
|
|
868
|
+
r = _gI(self.lat1, self.lon1, lat0, lon0, outmask=Cs.AZIMUTH_DISTANCE)
|
|
869
|
+
d, _ = _diff182(r.azi2, self.azi12, K_2_0=True)
|
|
870
|
+
_, s12 = sincos2d(d)
|
|
871
|
+
s12 *= r.s12 # signed
|
|
872
|
+
else:
|
|
873
|
+
s12 = Meter(est=est)
|
|
874
|
+
try:
|
|
875
|
+
_abs = fabs
|
|
876
|
+
_d2 = _diff182
|
|
877
|
+
_ErT = E.rocPrimeVertical # aka rocTransverse
|
|
878
|
+
_ovr = _over
|
|
879
|
+
_S12 = Fsum(s12).fsum2_
|
|
880
|
+
_scd = sincos2d_
|
|
881
|
+
for i in range(1, _TRIPS): # 9+, suffix 1 == C++ 2, 2 == C++ 3
|
|
882
|
+
P = self.Position(s12) # outmask=Cs.LATITUDE_LONGITUDE
|
|
883
|
+
r = _gI(lat0, lon0, P.lat2, P.lon2, outmask=gm)
|
|
884
|
+
d, _ = _d2(self.azi12, r.azi2, K_2_0=True)
|
|
885
|
+
s, c, s2, c2 = _scd(d, r.lat2)
|
|
886
|
+
c2 *= _ErT(r.lat2)
|
|
887
|
+
s *= _ovr(s2 * self._salp, c2) - _ovr(s * r.M21, r.m12)
|
|
888
|
+
s12, t = _S12(c / s) # XXX _ovr?
|
|
889
|
+
if _abs(t) < tol: # or fabs(c) < EPS
|
|
890
|
+
break
|
|
891
|
+
P.set_(azi0=r.azi1, a02=r.a12, s02=r.s12, # azi2=r.azi2,
|
|
892
|
+
lat0=lat0, lon0=lon0, iteration=i, at=r.azi2 - self.azi12,
|
|
893
|
+
name=_dunder_nameof(self.PlumbTo, self.name))
|
|
894
|
+
except Exception as x: # Fsum(NAN) Value-, ZeroDivisionError
|
|
895
|
+
raise IntersectionError(lat0, lon0, tol=tol, exact=exact,
|
|
896
|
+
eps=eps, est=est, iteration=i, cause=x)
|
|
897
|
+
|
|
898
|
+
return P
|
|
899
|
+
|
|
900
|
+
def Position(self, s12, outmask=Caps.LATITUDE_LONGITUDE):
|
|
901
|
+
'''Compute a point at a given distance on this rhumb line.
|
|
902
|
+
|
|
903
|
+
@arg s12: The distance along this rhumb line from its origin to
|
|
904
|
+
the point (C{meters}), can be negative.
|
|
905
|
+
@kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
|
|
906
|
+
the quantities to be returned.
|
|
907
|
+
|
|
908
|
+
@return: L{GDict} with 4 to 8 items C{azi12, a12, s12, S12, lat2,
|
|
909
|
+
lat1, lon2, lon1} with latitude C{lat2} and longitude
|
|
910
|
+
C{lon2} of the point in C{degrees}, the rhumb angle C{a12}
|
|
911
|
+
in C{degrees} from the start point of and the area C{S12}
|
|
912
|
+
under this rhumb line in C{meter} I{squared}.
|
|
913
|
+
|
|
914
|
+
@raise ImportError: Package C{numpy} not found or not installed,
|
|
915
|
+
only required for L{RhumbLineAux} area C{S12}
|
|
916
|
+
when C{B{exact} is True}.
|
|
917
|
+
|
|
918
|
+
@note: If B{C{s12}} is large enough that the rhumb line crosses a
|
|
919
|
+
pole, the longitude of the second point is indeterminate and
|
|
920
|
+
C{NAN} is returned for C{lon2} and area C{S12}.
|
|
921
|
+
|
|
922
|
+
If the first point is a pole, the cosine of its latitude is
|
|
923
|
+
taken to be C{sqrt(L{EPS})}. This position is extremely
|
|
924
|
+
close to the actual pole and allows the calculation to be
|
|
925
|
+
carried out in finite terms.
|
|
926
|
+
'''
|
|
927
|
+
return self._Position(self.m2degrees(s12), s12, outmask)
|
|
928
|
+
|
|
929
|
+
def _Position(self, a12, s12, outmask):
|
|
930
|
+
'''(INTERNAL) C{Arc-/Position} helper.
|
|
931
|
+
'''
|
|
932
|
+
r = GDict(azi12=self.azi12, a12=a12, s12=s12, name=self.name)
|
|
933
|
+
Cs = Caps
|
|
934
|
+
if (outmask & Cs.LATITUDE_LONGITUDE_AREA):
|
|
935
|
+
if a12 or s12:
|
|
936
|
+
mu12 = self._calp * a12
|
|
937
|
+
mu2 = self._mu1 + mu12
|
|
938
|
+
if fabs(mu2) > 90: # past pole
|
|
939
|
+
mu2 = _norm180(mu2) # reduce to [-180, 180)
|
|
940
|
+
if fabs(mu2) > 90: # point on anti-meridian
|
|
941
|
+
mu2 = _norm180(_loneg(mu2))
|
|
942
|
+
lat2 = self._mu2lat(mu2)
|
|
943
|
+
lon2 = S12 = NAN
|
|
944
|
+
else:
|
|
945
|
+
lat2, lon2, S1, S2 = self._Position4(a12, mu2, s12, mu12)
|
|
946
|
+
if (outmask & Cs.AREA):
|
|
947
|
+
S12 = self.rhumb._S12d(S1, S2, lon2)
|
|
948
|
+
S12 = unsigned0(S12) # like .gx
|
|
949
|
+
# else:
|
|
950
|
+
# S12 = None # unused
|
|
951
|
+
if (outmask & Cs.LONGITUDE):
|
|
952
|
+
if (outmask & Cs.LONG_UNROLL):
|
|
953
|
+
lon2 += self.lon1
|
|
954
|
+
else:
|
|
955
|
+
lon2 = _norm180(self._lon12 + lon2)
|
|
956
|
+
else: # coincident
|
|
957
|
+
lat2, lon2 = self.latlon1
|
|
958
|
+
S12 = _0_0
|
|
959
|
+
|
|
960
|
+
if (outmask & Cs.AREA):
|
|
961
|
+
r.set_(S12=S12)
|
|
962
|
+
if (outmask & Cs.LATITUDE):
|
|
963
|
+
r.set_(lat2=lat2, lat1=self.lat1)
|
|
964
|
+
if (outmask & Cs.LONGITUDE):
|
|
965
|
+
r.set_(lon2=lon2, lon1=self.lon1)
|
|
966
|
+
return r
|
|
967
|
+
|
|
968
|
+
def _Position4(self, a12, mu2, s12, mu12): # PYCHOK no cover
|
|
969
|
+
'''(INTERNAL) I{Must be overloaded}.'''
|
|
970
|
+
_MODS.named.notOverloaded(self, a12, s12, mu2, mu12)
|
|
971
|
+
|
|
972
|
+
@Property_RO
|
|
973
|
+
def rhumb(self):
|
|
974
|
+
'''Get this rhumb line's rhumb (L{RhumbAux} or L{Rhumb}).
|
|
975
|
+
'''
|
|
976
|
+
return self._rhumb
|
|
977
|
+
|
|
978
|
+
def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
|
|
979
|
+
'''Return this C{RhumbLine} as string.
|
|
980
|
+
|
|
981
|
+
@kwarg prec: The C{float} precision, number of decimal digits (0..9).
|
|
982
|
+
Trailing zero decimals are stripped for B{C{prec}} values
|
|
983
|
+
of 1 and above, but kept for negative B{C{prec}} values.
|
|
984
|
+
@kwarg sep: Separator to join (C{str}).
|
|
985
|
+
|
|
986
|
+
@return: C{RhumbLine} (C{str}).
|
|
987
|
+
'''
|
|
988
|
+
d = dict(rhumb=self.rhumb, lat1=self.lat1, lon1=self.lon1,
|
|
989
|
+
azi12=self.azi12, exact=self.exact,
|
|
990
|
+
TMorder=self.TMorder, xTM=self.xTM)
|
|
991
|
+
return sep.join(pairs(itemsorted(d, asorted=False), prec=prec))
|
|
992
|
+
|
|
993
|
+
@property_RO
|
|
994
|
+
def TMorder(self):
|
|
995
|
+
'''Get this rhumb line's I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).
|
|
996
|
+
'''
|
|
997
|
+
return self.rhumb.TMorder
|
|
998
|
+
|
|
999
|
+
@Property_RO
|
|
1000
|
+
def xTM(self):
|
|
1001
|
+
'''Get this rhumb line's I{Transverse Mercator} projection (L{ExactTransverseMercator}
|
|
1002
|
+
if I{exact} and I{ellipsoidal}, otherwise L{KTransverseMercator} for C{TMorder}).
|
|
1003
|
+
'''
|
|
1004
|
+
E = self.ellipsoid
|
|
1005
|
+
# ExactTransverseMercator doesn't handle spherical earth models
|
|
1006
|
+
return _MODS.etm.ExactTransverseMercator(E) if self.exact and E.isEllipsoidal else \
|
|
1007
|
+
_MODS.ktm.KTransverseMercator(E, TMorder=self.TMorder)
|
|
1008
|
+
|
|
1009
|
+
def _xTM3d(self, latlon0, z=INT0, V3d=Vector3d):
|
|
1010
|
+
'''(INTERNAL) C{xTM.forward} this C{latlon1} to C{V3d} with B{C{latlon0}}
|
|
1011
|
+
as current intersection estimate and central meridian.
|
|
1012
|
+
'''
|
|
1013
|
+
t = self.xTM.forward(self.lat1 - latlon0.lat, self.lon1, lon0=latlon0.lon)
|
|
1014
|
+
return V3d(t.easting, t.northing, z)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
class _PseudoRhumbLine(RhumbLineBase):
|
|
1018
|
+
'''(INTERNAL) Pseudo-rhumb line for a geodesic (line), see C{geodesicw._PlumbTo}.
|
|
1019
|
+
'''
|
|
1020
|
+
def __init__(self, gl, name=NN):
|
|
1021
|
+
R = RhumbBase(gl.geodesic.ellipsoid, None, True, name)
|
|
1022
|
+
RhumbLineBase.__init__(self, R, gl.lat1, gl.lon1, 0, caps=Caps.LINE_OFF)
|
|
1023
|
+
self._azi1 = self.azi12 = gl.azi1
|
|
1024
|
+
self._gl = gl
|
|
1025
|
+
self._gD = gl.geodesic.Direct
|
|
1026
|
+
|
|
1027
|
+
def PlumbTo(self, lat0, lon0, **exact_eps_est_tol): # PYCHOK signature
|
|
1028
|
+
P = RhumbLineBase.PlumbTo(self, lat0, lon0, **exact_eps_est_tol)
|
|
1029
|
+
z, P = _xkwds_pop2(P, azi12=None)
|
|
1030
|
+
P.set_(azi1=self._gl.azi1, azi2=z)
|
|
1031
|
+
return P # geodesic L{Position}
|
|
1032
|
+
|
|
1033
|
+
def Position(self, s12, **unused): # PYCHOK signature
|
|
1034
|
+
r = self._gD(self.lat1, self.lon1, self._azi1, s12)
|
|
1035
|
+
self._azi1 = r.azi1
|
|
1036
|
+
self.azi12 = z = r.azi2
|
|
1037
|
+
self._salp, _ = sincos2d(z)
|
|
1038
|
+
return r.set_(azi12=z)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
__all__ += _ALL_DOCS(RhumbBase, RhumbLineBase)
|
|
1042
|
+
|
|
1043
|
+
if __name__ == '__main__':
|
|
1044
|
+
|
|
1045
|
+
from pygeodesy import printf, Rhumb as Rh, RhumbAux as Ah
|
|
1046
|
+
from pygeodesy.basics import _zip
|
|
1047
|
+
from pygeodesy.ellipsoids import _EWGS84
|
|
1048
|
+
|
|
1049
|
+
Al = Ah(_EWGS84).Line(30, 0, 45)
|
|
1050
|
+
Rl = Rh(_EWGS84).Line(30, 0, 45)
|
|
1051
|
+
|
|
1052
|
+
for i in range(1, 10):
|
|
1053
|
+
s = .5e6 + 1e6 / i
|
|
1054
|
+
a = Al.Position(s).lon2
|
|
1055
|
+
r = Rl.Position(s).lon2
|
|
1056
|
+
e = (fabs(a - r) / a) if a else 0
|
|
1057
|
+
printf('# Position.lon2 %.14f vs %.14f, diff %g', r, a, e)
|
|
1058
|
+
|
|
1059
|
+
for exact in (None, False, True):
|
|
1060
|
+
for est in (None, 1e6):
|
|
1061
|
+
a = Al.PlumbTo(60, 0, exact=exact, est=est)
|
|
1062
|
+
r = Rl.PlumbTo(60, 0, exact=exact, est=est)
|
|
1063
|
+
printf('# %s, iteration=%s, exact=%s, est=%s\n# %s, iteration=%s',
|
|
1064
|
+
a.toRepr(), a.iteration, exact, est,
|
|
1065
|
+
r.toRepr(), r.iteration, nl=1)
|
|
1066
|
+
|
|
1067
|
+
NE_=(71.688899882813, 0.2555198244234, 44095641862956.11)
|
|
1068
|
+
LHR=(77.7683897102557, 5771083.38332803, 37395209100030.39)
|
|
1069
|
+
NRT=(-92.38888798169965, 12782581.067684170, -63760642939072.50)
|
|
1070
|
+
|
|
1071
|
+
def _ref(fmt, r3, x3):
|
|
1072
|
+
e3 = []
|
|
1073
|
+
for r, x in _zip(r3, x3): # strict=True
|
|
1074
|
+
e = fabs(r - x) / fabs(x)
|
|
1075
|
+
e3.append('%.g' % (e,))
|
|
1076
|
+
printf((fmt % r3) + ', rel errors: ' + ', '.join(e3))
|
|
1077
|
+
|
|
1078
|
+
for R in (Ah, Rh): # <https://GeographicLib.SourceForge.io/cgi-bin/RhumbSolve -p 9> version 2.2
|
|
1079
|
+
rh = R(exact=True) # WGS84 default
|
|
1080
|
+
printf('# %r', rh, nl=1)
|
|
1081
|
+
r = rh.Direct8(40.6, -73.8, 51, 5.5e6) # from JFK about NE
|
|
1082
|
+
_ref('# JFK NE lat2=%.12f, lon2=%.12f, S12=%.1f', (r.lat2, r.lon2, r.S12), NE_)
|
|
1083
|
+
r = rh.Inverse8(40.6, -73.8, 51.6, -0.5) # JFK to LHR
|
|
1084
|
+
_ref('# JFK-LHR azi12=%.12f, s12=%.3f S12=%.1f', (r.azi12, r.s12, r.S12), LHR)
|
|
1085
|
+
r = rh.Inverse8(40.6, -73.8, 35.8, 140.3) # JFK to Tokyo Narita
|
|
1086
|
+
_ref('# JFK-NRT azi12=%.12f, s12=%.3f S12=%.1f', (r.azi12, r.s12, r.S12), NRT)
|
|
1087
|
+
|
|
1088
|
+
# % python3 -m pygeodesy.rhumb.bases
|
|
1089
|
+
|
|
1090
|
+
# Position.lon2 11.61455846901637 vs 11.61455846901637, diff 3.05885e-16
|
|
1091
|
+
# Position.lon2 7.58982302826842 vs 7.58982302826842, diff 2.34045e-16
|
|
1092
|
+
# Position.lon2 6.28526067416369 vs 6.28526067416369, diff 2.82623e-16
|
|
1093
|
+
# Position.lon2 5.63938995325146 vs 5.63938995325146, diff 1.57495e-16
|
|
1094
|
+
# Position.lon2 5.25385527435707 vs 5.25385527435707, diff 0
|
|
1095
|
+
# Position.lon2 4.99764604290380 vs 4.99764604290380, diff 8.88597e-16
|
|
1096
|
+
# Position.lon2 4.81503363740473 vs 4.81503363740473, diff 1.84459e-16
|
|
1097
|
+
# Position.lon2 4.67828821748836 vs 4.67828821748835, diff 5.69553e-16
|
|
1098
|
+
# Position.lon2 4.57205667906283 vs 4.57205667906283, diff 5.82787e-16
|
|
1099
|
+
|
|
1100
|
+
# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9, exact=None, est=None
|
|
1101
|
+
# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9
|
|
1102
|
+
|
|
1103
|
+
# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9, exact=None, est=1000000.0
|
|
1104
|
+
# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9
|
|
1105
|
+
|
|
1106
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5, exact=False, est=None
|
|
1107
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5
|
|
1108
|
+
|
|
1109
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7, exact=False, est=1000000.0
|
|
1110
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7
|
|
1111
|
+
|
|
1112
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5, exact=True, est=None
|
|
1113
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5
|
|
1114
|
+
|
|
1115
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7, exact=True, est=1000000.0
|
|
1116
|
+
# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7
|
|
1117
|
+
|
|
1118
|
+
# RhumbAux(RAorder=None, TMorder=6, ellipsoid=Ellipsoid(name='WGS84', a=6378137, b=6356752.31424518, f_=298.25722356, f=0.00335281, f2=0.00336409, n=0.00167922, e=0.08181919, e2=0.00669438, e21=0.99330562, e22=0.0067395, e32=0.00335843, A=6367449.14582341, L=10001965.72931272, R1=6371008.77141506, R2=6371007.18091847, R3=6371000.79000916, Rbiaxial=6367453.63451633, Rtriaxial=6372797.5559594), exact=True)
|
|
1119
|
+
# JFK NE lat2=71.688899882813, lon2=0.255519824423, S12=44095641862956.1, rel errors: 4e-16, 2e-13, 4e-16
|
|
1120
|
+
# JFK-LHR azi12=77.768389710256, s12=5771083.383 S12=37395209100030.4, rel errors: 5e-16, 3e-16, 8e-16
|
|
1121
|
+
# JFK-NRT azi12=-92.388887981700, s12=12782581.068 S12=-63760642939072.5, rel errors: 0, 1e-16, 7e-16
|
|
1122
|
+
|
|
1123
|
+
# Rhumb(RAorder=6, TMorder=6, ellipsoid=Ellipsoid(name='WGS84', a=6378137, b=6356752.31424518, f_=298.25722356, f=0.00335281, f2=0.00336409, n=0.00167922, e=0.08181919, e2=0.00669438, e21=0.99330562, e22=0.0067395, e32=0.00335843, A=6367449.14582341, L=10001965.72931272, R1=6371008.77141506, R2=6371007.18091847, R3=6371000.79000916, Rbiaxial=6367453.63451633, Rtriaxial=6372797.5559594), exact=True)
|
|
1124
|
+
# JFK NE lat2=71.688899882813, lon2=0.255519824423, S12=44095641862956.1, rel errors: 2e-16, 1e-13, 5e-16
|
|
1125
|
+
# JFK-LHR azi12=77.768389710256, s12=5771083.383 S12=37395209100030.4, rel errors: 4e-16, 3e-16, 6e-16
|
|
1126
|
+
# JFK-NRT azi12=-92.388887981700, s12=12782581.068 S12=-63760642939072.5, rel errors: 0, 1e-16, 1e-16
|
|
1127
|
+
|
|
1128
|
+
# **) MIT License
|
|
1129
|
+
#
|
|
1130
|
+
# Copyright (C) 2022-2024 -- mrJean1 at Gmail -- All Rights Reserved.
|
|
1131
|
+
#
|
|
1132
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
1133
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
1134
|
+
# to deal in the Software without restriction, including without limitation
|
|
1135
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
1136
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
1137
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
1138
|
+
#
|
|
1139
|
+
# The above copyright notice and this permission notice shall be included
|
|
1140
|
+
# in all copies or substantial portions of the Software.
|
|
1141
|
+
#
|
|
1142
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
1143
|
+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
1144
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
1145
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
1146
|
+
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
1147
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
1148
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|