Coverage for pygeodesy/sphericalBase.py: 95%
217 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-09-20 13:43 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-09-20 13:43 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private spherical base classes C{CartesianSphericalBase} and
5C{LatLonSphericalBase} for L{sphericalNvector} and L{sphericalTrigonometry}.
7A pure Python implementation of geodetic (lat-/longitude) functions,
8transcoded in part from JavaScript originals by I{(C) Chris Veness 2011-2016}
9and published under the same MIT Licence**, see
10U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>}.
11'''
12# make sure int/int division yields float quotient, see .basics
13from __future__ import division as _; del _ # PYCHOK semicolon
15from pygeodesy.basics import isbool, isinstanceof, map1
16from pygeodesy.cartesianBase import CartesianBase, Bearing2Tuple
17from pygeodesy.constants import EPS, PI, PI2, PI_2, R_M, \
18 _umod_360, isnear0, isnon0, _0_0, \
19 _0_5, _1_0, _180_0
20from pygeodesy.datums import Datums, _spherical_datum
21from pygeodesy.errors import IntersectionError, _ValueError, _xError
22from pygeodesy.fmath import favg, fdot, hypot, sqrt_a
23from pygeodesy.interns import NN, _COMMA_, _concentric_, _datum_, \
24 _distant_, _exceed_PI_radians_, _name_, \
25 _near_, _radius_, _too_
26from pygeodesy.latlonBase import LatLonBase, _trilaterate5 # PYCHOK passed
27from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
28# from pygeodesy.namedTuples import Bearing2Tuple # from .cartesianBase
29from pygeodesy.nvectorBase import NvectorBase, Fmt, _xattrs
30from pygeodesy.props import deprecated_method, property_doc_, \
31 property_RO, _update_all
32# from pygeodesy.streprs import Fmt, _xattrs # from .nvectorBase
33from pygeodesy.units import Bearing, Bearing_, Radians_, Radius, \
34 Radius_, Scalar_
35from pygeodesy.utily import acos1, atan2b, atan2d, degrees90, \
36 degrees180, sincos2, sincos2d, tanPI_2_2, \
37 _unrollon, wrap360, wrapPI
39from math import cos, fabs, log, sin, sqrt
41__all__ = _ALL_LAZY.sphericalBase
42__version__ = '23.07.01'
45def _angular(distance, radius, low=EPS): # PYCHOK in .spherical*
46 '''(INTERNAL) Return an angular distance in C{radians}.
48 @raise UnitError: Invalid B{C{distance}} or B{C{radius}}.
49 '''
50 r = float(distance)
51 if radius:
52 r = r / Radius_(radius=radius) # /= chokes PyChecker
53 if low is not None:
54 # small near0 values from .rhumbDestination not exact OK
55 r = _0_0 if low < 0 and low < r < 0 else Radians_(r, low=low)
56 return r
59def _rads3(rad1, rad2, radius): # in .sphericalTrigonometry
60 '''(INTERNAL) Convert radii to radians.
61 '''
62 r1 = Radius_(rad1=rad1)
63 r2 = Radius_(rad2=rad2)
64 if radius is not None: # convert radii to radians
65 r1 = _angular(r1, radius)
66 r2 = _angular(r2, radius)
68 x = r1 < r2
69 if x:
70 r1, r2 = r2, r1
71 if r1 > PI:
72 raise IntersectionError(rad1=rad1, rad2=rad2,
73 txt=_exceed_PI_radians_)
74 return r1, r2, x
77class CartesianSphericalBase(CartesianBase):
78 '''(INTERNAL) Base class for spherical C{Cartesian}s.
79 '''
80 _datum = Datums.Sphere # L{Datum}
82 def intersections2(self, rad1, other, rad2, radius=R_M):
83 '''Compute the intersection points of two circles each defined
84 by a center point and a radius.
86 @arg rad1: Radius of the this circle (C{meter} or C{radians},
87 see B{C{radius}}).
88 @arg other: Center of the other circle (C{Cartesian}).
89 @arg rad2: Radius of the other circle (C{meter} or C{radians},
90 see B{C{radius}}).
91 @kwarg radius: Mean earth radius (C{meter} or C{None} if both
92 B{C{rad1}} and B{C{rad2}} are given in C{radians}).
94 @return: 2-Tuple of the intersection points, each C{Cartesian}.
95 For abutting circles, the intersection points are the
96 same C{Cartesian} instance, aka the I{radical center}.
98 @raise IntersectionError: Concentric, antipodal, invalid or
99 non-intersecting circles.
101 @raise TypeError: If B{C{other}} is not C{Cartesian}.
103 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}} or B{C{radius}}.
105 @see: U{Calculating intersection of two Circles
106 <https://GIS.StackExchange.com/questions/48937/
107 calculating-intersection-of-two-circles>} and method
108 or function C{trilaterate3d2}.
109 '''
110 x1, x2 = self, self.others(other)
111 r1, r2, x = _rads3(rad1, rad2, radius)
112 if x:
113 x1, x2 = x2, x1
114 try:
115 n, q = x1.cross(x2), x1.dot(x2)
116 n2, q1 = n.length2, (_1_0 - q**2)
117 if n2 < EPS or isnear0(q1):
118 raise ValueError(_near_(_concentric_))
119 c1, c2 = cos(r1), cos(r2)
120 x0 = x1.times((c1 - q * c2) / q1).plus(
121 x2.times((c2 - q * c1) / q1))
122 n1 = _1_0 - x0.length2
123 if n1 < EPS:
124 raise ValueError(_too_(_distant_))
125 except ValueError as x:
126 raise IntersectionError(center=self, rad1=rad1,
127 other=other, rad2=rad2, cause=x)
128 n = n.times(sqrt(n1 / n2))
129 if n.length > EPS:
130 x1 = x0.plus(n)
131 x2 = x0.minus(n)
132 else: # abutting circles
133 x1 = x2 = x0
135 return (_xattrs(x1, self, _datum_, _name_),
136 _xattrs(x2, self, _datum_, _name_))
139class LatLonSphericalBase(LatLonBase):
140 '''(INTERNAL) Base class for spherical C{LatLon}s.
141 '''
142 _datum = Datums.Sphere # spherical L{Datum}
144 def __init__(self, latlonh, lon=None, height=0, datum=None, wrap=False, name=NN):
145 '''Create a spherical C{LatLon} point frome the given lat-, longitude and
146 height on the given datum.
148 @arg latlonh: Latitude (C{degrees} or DMS C{str} with N or S suffix) or
149 a previous C{LatLon} instance provided C{B{lon}=None}.
150 @kwarg lon: Longitude (C{degrees} or DMS C{str} with E or W suffix) or
151 C(None), indicating B{C{latlonh}} is a C{LatLon}.
152 @kwarg height: Optional height above (or below) the earth surface (C{meter},
153 same units as the datum's ellipsoid axes or radius).
154 @kwarg datum: Optional, spherical datum to use (L{Datum}, L{Ellipsoid},
155 L{Ellipsoid2}, L{a_f2Tuple}) or earth radius in C{meter},
156 conventionally).
157 @kwarg wrap: If C{True}, wrap or I{normalize} B{C{lat}} and B{C{lon}}
158 (C{bool}).
159 @kwarg name: Optional name (string).
161 @raise TypeError: If B{C{latlonh}} is not a C{LatLon} or B{C{datum}} not
162 spherical.
163 '''
164 LatLonBase.__init__(self, latlonh, lon=lon, height=height, wrap=wrap, name=name)
165 if datum not in (None, self.datum):
166 self.datum = datum
168 def bearingTo2(self, other, wrap=False, raiser=False):
169 '''Return the initial and final bearing (forward and reverse
170 azimuth) from this to an other point.
172 @arg other: The other point (C{LatLon}).
173 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
174 B{C{other}} point (C{bool}).
176 @return: A L{Bearing2Tuple}C{(initial, final)}.
178 @raise TypeError: The B{C{other}} point is not spherical.
180 @see: Methods C{initialBearingTo} and C{finalBearingTo}.
181 '''
182 # .initialBearingTo is inside .-Nvector and .-Trigonometry
183 i = self.initialBearingTo(other, wrap=wrap, raiser=raiser) # PYCHOK .initialBearingTo
184 f = self.finalBearingTo( other, wrap=wrap, raiser=raiser)
185 return Bearing2Tuple(i, f, name=self.name)
187 @property_doc_(''' this point's datum (L{Datum}).''')
188 def datum(self):
189 '''Get this point's datum (L{Datum}).
190 '''
191 return self._datum
193 @datum.setter # PYCHOK setter!
194 def datum(self, datum):
195 '''Set this point's datum I{without conversion} (L{Datum}, L{Ellipsoid},
196 L{Ellipsoid2}, L{a_f2Tuple}) or C{scalar} spherical earth radius).
198 @raise TypeError: If B{C{datum}} invalid or not not spherical.
199 '''
200 d = _spherical_datum(datum, name=self.name, raiser=_datum_)
201 if self._datum != d:
202 _update_all(self)
203 self._datum = d
205 def finalBearingTo(self, other, wrap=False, raiser=False):
206 '''Return the final bearing (reverse azimuth) from this to
207 an other point.
209 @arg other: The other point (spherical C{LatLon}).
210 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
211 the B{C{other}} point (C{bool}).
213 @return: Final bearing (compass C{degrees360}).
215 @raise TypeError: The B{C{other}} point is not spherical.
217 @example:
219 >>> p = LatLon(52.205, 0.119)
220 >>> q = LatLon(48.857, 2.351)
221 >>> b = p.finalBearingTo(q) # 157.9
222 '''
223 p = self.others(other)
224 if wrap:
225 p = _unrollon(self, p, wrap=wrap)
226 # final bearing is the reverse of the other, initial one;
227 # .initialBearingTo is inside .-Nvector and .-Trigonometry
228 b = p.initialBearingTo(self, wrap=False, raiser=raiser)
229 return _umod_360(b + _180_0)
231 def intersecant2(self, circle, point, bearing, radius=R_M, exact=False,
232 height=None, wrap=False): # was=True
233 '''Compute the intersections of a circle and a line.
235 @arg circle: Radius of the circle centered at this location
236 (C{meter}, same units as B{C{radius}}) or a point
237 on the circle (this C{LatLon}).
238 @arg point: An other point in- or outside the circle (this C{LatLon}).
239 @arg bearing: Bearing at the B{C{point}} (compass C{degrees360})
240 or a second point on the line (this C{LatLon}).
241 @kwarg radius: Mean earth radius (C{meter}, conventionally).
242 @kwarg exact: If C{True} use the I{exact} rhumb methods for azimuth,
243 destination and distance, if C{False} use the basic
244 rhumb methods (C{bool}) or if C{None} use the I{great
245 circle} methods.
246 @kwarg height: Optional height for the intersection points (C{meter},
247 conventionally) or C{None}.
248 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
249 B{C{point}}, B{C{circle}} and/or B{C{bearing}} (C{bool}).
251 @return: 2-Tuple of the intersection points (representing a chord),
252 each an instance of this class. For a tangent line, both
253 points are the same instance, the B{C{point}} or wrapped
254 or I{normalized}.
256 @raise IntersectionError: The circle and line do not intersect.
258 @raise TypeError: If B{C{point}} is not this C{LatLon} or B{C{circle}}
259 or B{C{bearing}} invalid.
261 @raise ValueError: Invalid B{C{circle}}, B{C{bearing}}, B{C{radius}},
262 B{C{exact}} or B{C{height}}.
263 '''
264 p = self.others(point=point)
265 try:
266 return _intersecant2(self, circle, p, bearing, radius=radius, exact=exact,
267 height=height, wrap=wrap)
268 except (TypeError, ValueError) as x:
269 raise _xError(x, center=self, circle=circle, point=point, bearing=bearing,
270 exact=exact, wrap=wrap)
272 def maxLat(self, bearing):
273 '''Return the maximum latitude reached when travelling
274 on a great circle on given bearing from this point
275 based on Clairaut's formula.
277 The maximum latitude is independent of longitude
278 and the same for all points on a given latitude.
280 Negate the result for the minimum latitude (on the
281 Southern hemisphere).
283 @arg bearing: Initial bearing (compass C{degrees360}).
285 @return: Maximum latitude (C{degrees90}).
287 @raise ValueError: Invalid B{C{bearing}}.
288 '''
289 m = acos1(fabs(sin(Bearing_(bearing)) * cos(self.phi)))
290 return degrees90(m)
292 def minLat(self, bearing):
293 '''Return the minimum latitude reached when travelling
294 on a great circle on given bearing from this point.
296 @arg bearing: Initial bearing (compass C{degrees360}).
298 @return: Minimum latitude (C{degrees90}).
300 @see: Method L{maxLat} for more details.
302 @raise ValueError: Invalid B{C{bearing}}.
303 '''
304 return -self.maxLat(bearing)
306 def parse(self, strllh, height=0, sep=_COMMA_, name=NN):
307 '''Parse a string representing a similar, spherical C{LatLon}
308 point, consisting of C{"lat, lon[, height]"}.
310 @arg strllh: Lat, lon and optional height (C{str}),
311 see function L{pygeodesy.parse3llh}.
312 @kwarg height: Optional, default height (C{meter}).
313 @kwarg sep: Optional separator (C{str}).
314 @kwarg name: Optional instance name (C{str}),
315 overriding this name.
317 @return: The similar point (spherical C{LatLon}).
319 @raise ParseError: Invalid B{C{strllh}}.
320 '''
321 t = _MODS.dms.parse3llh(strllh, height=height, sep=sep)
322 r = self.classof(*t)
323 if name:
324 r.rename(name)
325 return r
327 @property_RO
328 def _radius(self):
329 '''(INTERNAL) Get this sphere's radius.
330 '''
331 return self.datum.ellipsoid.equatoradius
333 def _rhumbs3(self, other, wrap, r=False): # != .latlonBase._rhumbx3
334 '''(INTERNAL) Rhumb_ helper function.
336 @arg other: The other point (spherical C{LatLon}).
337 '''
338 p = self.others(other, up=2)
339 if wrap:
340 p = _unrollon(self, p, wrap=wrap)
341 a2, b2 = p.philam
342 a1, b1 = self.philam
343 # if |db| > 180 take shorter rhumb
344 # line across the anti-meridian
345 db = wrapPI(b2 - b1)
346 dp = log(tanPI_2_2(a2) / tanPI_2_2(a1))
347 da = a2 - a1
348 if r:
349 # on Mercator projection, longitude distances shrink
350 # by latitude; the 'stretch factor' q becomes ill-
351 # conditioned along E-W line (0/0); use an empirical
352 # tolerance to avoid it
353 q = (da / dp) if fabs(dp) > EPS else cos(self.phi)
354 da = hypot(da, q * db) # angular distance radians
355 return da, db, dp
357 def rhumbAzimuthTo(self, other, radius=R_M, exact=False, wrap=False):
358 '''Return the azimuth (bearing) of a rhumb line (loxodrome)
359 between this and an other (spherical) point.
361 @arg other: The other point (spherical C{LatLon}).
362 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},
363 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
364 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),
365 default C{False} for backward compatibility.
366 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
367 B{C{other}} point (C{bool}).
369 @return: Rhumb line azimuth (compass C{degrees180}).
371 @raise TypeError: The B{C{other}} point is incompatible or
372 B{C{radius}} is invalid.
374 @example:
376 >>> p = LatLon(51.127, 1.338)
377 >>> q = LatLon(50.964, 1.853)
378 >>> b = p.rhumbBearingTo(q) # 116.7
379 '''
380 if exact: # use series, always
381 z = LatLonBase.rhumbAzimuthTo(self, other, exact=False, # Krüger
382 radius=radius, wrap=wrap)
383 else:
384 _, db, dp = self._rhumbs3(other, wrap)
385 z = atan2d(db, dp) # see .rhumbx.Rhumb.Inverse
386 return z
388 @deprecated_method
389 def rhumbBearingTo(self, other): # unwrapped
390 '''DEPRECATED, use method C{.rhumbAzimuthTo}.'''
391 return wrap360(self.rhumbAzimuthTo(other)) # [0..360)
393 def rhumbDestination(self, distance, bearing, radius=R_M, height=None, exact=False):
394 '''Return the destination point having travelled the given distance
395 from this point along a rhumb line (loxodrome) at the given bearing.
397 @arg distance: Distance travelled (C{meter}, same units as B{C{radius}}),
398 may be negative if C{B{exact}=True}.
399 @arg bearing: Bearing (azimuth) at this point (compass C{degrees360}).
400 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},
401 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}) if
402 C{B{exact}=True}.
403 @kwarg height: Optional height, overriding the default height
404 (C{meter}, same unit as B{C{radius}}).
405 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),
406 default C{False} for backward compatibility.
408 @return: The destination point (spherical C{LatLon}).
410 @raise ValueError: Invalid B{C{distance}}, B{C{bearing}},
411 B{C{radius}} or B{C{height}}.
413 @example:
415 >>> p = LatLon(51.127, 1.338)
416 >>> q = p.rhumbDestination(40300, 116.7) # 50.9642°N, 001.8530°E
417 '''
418 if exact: # use series, always
419 r = LatLonBase.rhumbDestination(self, distance, bearing, exact=False, # Krüger
420 radius=radius, height=height)
421 else: # radius=None from .rhumbMidpointTo
422 if radius in (None, self._radius):
423 d, r = self.datum, radius
424 else:
425 d = _spherical_datum(radius, raiser=_radius_) # spherical only
426 r = d.ellipsoid.equatoradius
427 r = _angular(distance, r, low=-EPS) # distance=0 from .rhumbMidpointTo
429 a1, b1 = self.philam
430 sb, cb = sincos2(Bearing_(bearing))
432 da = r * cb
433 a2 = a1 + da
434 # normalize latitude if past pole
435 if a2 > PI_2:
436 a2 = PI - a2
437 elif a2 < -PI_2:
438 a2 = -PI - a2
440 dp = log(tanPI_2_2(a2) / tanPI_2_2(a1))
441 # q becomes ill-conditioned on E-W course 0/0
442 q = (da / dp) if fabs(dp) > EPS else cos(a1)
443 b2 = (b1 + r * sb / q) if fabs(q) > EPS else b1
445 h = self._heigHt(height)
446 r = self.classof(degrees90(a2), degrees180(b2), datum=d, height=h)
447 return r
449 def rhumbDistanceTo(self, other, radius=R_M, exact=False, wrap=False):
450 '''Return the distance from this to an other point along
451 a rhumb line (loxodrome).
453 @arg other: The other point (spherical C{LatLon}).
454 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},
455 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}) if
456 C{B{exact}=True}.
457 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),
458 default C{False} for backward compatibility.
459 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
460 B{C{other}} point (C{bool}).
462 @return: Distance (C{meter}, the same units as B{C{radius}}
463 or C{radians} if B{C{radius}} is C{None}).
465 @raise TypeError: The B{C{other}} point is incompatible.
467 @raise ValueError: Invalid B{C{radius}}.
469 @example:
471 >>> p = LatLon(51.127, 1.338)
472 >>> q = LatLon(50.964, 1.853)
473 >>> d = p.rhumbDistanceTo(q) # 403100
474 '''
475 if exact: # use series, always
476 r = LatLonBase.rhumbDistanceTo(self, other, exact=False, # Krüger
477 radius=radius, wrap=wrap)
478 if radius is None: # angular distance in radians
479 r = r / self._radius # /= chokes PyChecker
480 else:
481 # see <https://www.EdWilliams.org/avform.htm#Rhumb>
482 r, _, _ = self._rhumbs3(other, wrap, r=True)
483 if radius is not None:
484 r *= Radius(radius)
485 return r
487 def rhumbMidpointTo(self, other, height=None, radius=R_M, exact=False,
488 fraction=_0_5, wrap=False):
489 '''Return the (loxodromic) midpoint on the rhumb line between
490 this and an other point.
492 @arg other: The other point (spherical LatLon).
493 @kwarg height: Optional height, overriding the mean height
494 (C{meter}).
495 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},
496 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
497 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),
498 default C{False} for backward compatibility.
499 @kwarg fraction: Midpoint location from this point (C{scalar}),
500 may be negative if C{B{exact}=True}.
501 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
502 B{C{other}} point (C{bool}).
504 @return: The (mid)point at the given B{C{fraction}} along
505 the rhumb line (spherical C{LatLon}).
507 @raise TypeError: The B{C{other}} point is incompatible.
509 @raise ValueError: Invalid B{C{height}} or B{C{fraction}}
511 @example:
513 >>> p = LatLon(51.127, 1.338)
514 >>> q = LatLon(50.964, 1.853)
515 >>> m = p.rhumb_midpointTo(q)
516 >>> m.toStr() # '51.0455°N, 001.5957°E'
517 '''
518 if exact: # use series, always
519 r = LatLonBase.rhumbMidpointTo(self, other, exact=False, # Krüger
520 radius=radius, height=height,
521 fraction=fraction, wrap=wrap)
522 elif fraction is not _0_5:
523 f = Scalar_(fraction=fraction) # low=_0_0
524 r, db, dp = self._rhumbs3(other, wrap, r=True) # radians
525 z = atan2b(db, dp)
526 h = self._havg(other, f=f, h=height)
527 r = self.rhumbDestination(r * f, z, radius=None, height=h)
529 else: # for backward compatibility, unwrapped
530 # see <https://MathForum.org/library/drmath/view/51822.html>
531 a1, b1 = self.philam
532 a2, b2 = self.others(other).philam
534 if fabs(b2 - b1) > PI:
535 b1 += PI2 # crossing anti-meridian
537 a3 = favg(a1, a2)
538 b3 = favg(b1, b2)
540 f1 = tanPI_2_2(a1)
541 if isnon0(f1):
542 f2 = tanPI_2_2(a2)
543 f = f2 / f1
544 if isnon0(f):
545 f = log(f)
546 if isnon0(f):
547 f3 = tanPI_2_2(a3)
548 b3 = fdot(map1(log, f1, f2, f3),
549 -b2, b1, b2 - b1) / f
551 d = self.datum if radius in (None, self._radius) else \
552 _spherical_datum(radius, name=self.name, raiser=_radius_)
553 h = self._havg(other, h=height)
554 r = self.classof(degrees90(a3), degrees180(b3), datum=d, height=h)
555 return r
557 def toNvector(self, Nvector=NvectorBase, **Nvector_kwds): # PYCHOK signature
558 '''Convert this point to C{Nvector} components, I{including
559 height}.
561 @kwarg Nvector_kwds: Optional, additional B{C{Nvector}}
562 keyword arguments, ignored if
563 C{B{Nvector} is None}.
565 @return: An B{C{Nvector}} or a L{Vector4Tuple}C{(x, y, z, h)}
566 if B{C{Nvector}} is C{None}.
568 @raise TypeError: Invalid B{C{Nvector}} or B{C{Nvector_kwds}}.
569 '''
570 return LatLonBase.toNvector(self, Nvector=Nvector, **Nvector_kwds)
573def _intersecant2(c, r, p, b, radius=R_M, exact=False,
574 height=None, wrap=False):
575 # (INTERNAL) Intersect a circle and bearing, see L{intersecant2}
576 # above, separated to allow callers to embellish any exceptions
578 if wrap:
579 p = _unrollon(c, p, wrap=wrap)
580 nonexact = exact is None
582 if not isinstanceof(r, c.__class__, p.__class__):
583 r = Radius_(circle=r)
584 elif nonexact:
585 r = c.distanceTo(r, radius=radius, wrap=wrap)
586 elif isbool(exact):
587 r = c.rhumbDistanceTo(r, radius=radius, exact=exact, wrap=wrap)
588 else:
589 raise _ValueError(exact=exact)
591 if not isinstanceof(b, c.__class__, p.__class__):
592 b = Bearing(b)
593 elif nonexact:
594 b = p.initialBearingTo(b, wrap=wrap)
595 else:
596 b = p.rhumbAzimuthTo(b, radius=radius, exact=exact, wrap=wrap)
598 d = p.distanceTo(c, radius=radius) if nonexact else \
599 p.rhumbDistanceTo(c, radius=radius, exact=exact)
600 if d > EPS:
601 a = p.initialBearingTo(c) if nonexact else wrap360(
602 p.rhumbAzimuthTo(c, radius=radius, exact=exact))
603 s, c = sincos2d(b - a)
604 s = sqrt_a(r, fabs(s * d))
605 if s > r:
606 raise IntersectionError(_too_(Fmt.distant(s)))
607 elif (r - s) < EPS:
608 return p, p # tangent
609 c *= d
610 else: # p and c coincide
611 s, c = r, 0
612 t = ()
613 for d, b in ((s + c, b), (s - c, b + _180_0)): # bearing direction first
614 t += (p.destination(d, b, radius=radius, height=height) if nonexact else
615 p.rhumbDestination(d, b, radius=radius, height=height, exact=exact)),
616 return t
619def _r2m(r, radius):
620 '''(INTERNAL) Angular distance in C{radians} to C{meter}.
621 '''
622 if radius is not None: # not in (None, _0_0)
623 r *= R_M if radius is R_M else Radius(radius)
624 return r
627__all__ += _ALL_DOCS(CartesianSphericalBase, LatLonSphericalBase)
629# **) MIT License
630#
631# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
632#
633# Permission is hereby granted, free of charge, to any person obtaining a
634# copy of this software and associated documentation files (the "Software"),
635# to deal in the Software without restriction, including without limitation
636# the rights to use, copy, modify, merge, publish, distribute, sublicense,
637# and/or sell copies of the Software, and to permit persons to whom the
638# Software is furnished to do so, subject to the following conditions:
639#
640# The above copyright notice and this permission notice shall be included
641# in all copies or substantial portions of the Software.
642#
643# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
644# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
645# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
646# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
647# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
648# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
649# OTHER DEALINGS IN THE SOFTWARE.