1

2# -*- coding: utf-8 -*-

3

4 u'''(INTERNAL) Private spherical base classes C{CartesianSphericalBase} and

5 C{LatLonSphericalBase} for L{sphericalNvector} and L{sphericalTrigonometry}.

6

7 A pure Python implementation of geodetic (lat-/longitude) functions,

8 transcoded in part from JavaScript originals by I{(C) Chris Veness 2011-2016}

9 and published under the same MIT Licence**, see

10 U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>}.

11'''

12# make sure int/int division yields float quotient, see .basics

13 from __future__ import division as _; del _ # PYCHOK semicolon

14

15 from pygeodesy.basics import _copysign, isbool, isinstanceof, map1

16 from pygeodesy.cartesianBase import CartesianBase, Bearing2Tuple

17 from pygeodesy.constants import EPS, PI, PI2, PI_2, R_M, \

18 _umod_360, isnear0, isnon0, _over, \

19 _0_0, _0_5, _1_0, _180_0

20 from pygeodesy.datums import Datums, _spherical_datum

21 from pygeodesy.errors import IntersectionError, _ValueError, _xError

22 from pygeodesy.fmath import favg, fdot, hypot

23 from pygeodesy.interns import NN, _COMMA_, _concentric_, _datum_, \

24 _distant_, _exceed_PI_radians_, _name_, \

25 _near_, _radius_, _too_

26 from pygeodesy.latlonBase import _intersecend2, LatLonBase, \

27 _trilaterate5 # PYCHOK passed

28 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS

29# from pygeodesy.namedTuples import Bearing2Tuple # from .cartesianBase

30 from pygeodesy.nvectorBase import NvectorBase, _xattrs

31 from pygeodesy.props import deprecated_method, property_doc_, \

32 property_RO, _update_all

33# from pygeodesy.streprs import Fmt, _xattrs # from .nvectorBase

34 from pygeodesy.units import Bearing, Bearing_, Radians_, Radius, \

35 Radius_, Scalar_

36 from pygeodesy.utily import acos1, atan2b, atan2d, degrees90, \

37 degrees180, sincos2, _unrollon, \

38 tanPI_2_2, wrap360, wrapPI

39

40 from math import cos, fabs, log, sin, sqrt

41

42__all__ = _ALL_LAZY.sphericalBase

43__version__ = '23.10.08'

44

45

46 def _angular(distance, radius, low=EPS): # PYCHOK in .spherical*

47 '''(INTERNAL) Return an angular distance in C{radians}.

48

49 @raise UnitError: Invalid B{C{distance}} or B{C{radius}}.

50 '''

51 r = float(distance)

52 if radius:

53 r = r / Radius_(radius=radius) # /= chokes PyChecker

54 if low is not None:

55 # small near0 values from .rhumbDestination not exact OK

56 r = _0_0 if low < 0 and low < r < 0 else Radians_(r, low=low)

57 return r

58

59

60 def _logPI_2_2(a2, a1):

61 '''(INTERNAL) C{log} of C{tanPI_2_2}'s quotient.

62 '''

63 return log(_over(tanPI_2_2(a2), tanPI_2_2(a1)))

64

65

66 def _rads3(rad1, rad2, radius): # in .sphericalTrigonometry

67 '''(INTERNAL) Convert radii to radians.

68 '''

69 r1 = Radius_(rad1=rad1)

70 r2 = Radius_(rad2=rad2)

71 if radius is not None: # convert radii to radians

72 r1 = _angular(r1, radius)

73 r2 = _angular(r2, radius)

74

75 x = r1 < r2

76 if x:

77 r1, r2 = r2, r1

78 if r1 > PI:

79 raise IntersectionError(rad1=rad1, rad2=rad2,

80 txt=_exceed_PI_radians_)

81 return r1, r2, x

82

83

84 class CartesianSphericalBase(CartesianBase):

85 '''(INTERNAL) Base class for spherical C{Cartesian}s.

86 '''

87 _datum = Datums.Sphere # L{Datum}

88

89 def intersections2(self, rad1, other, rad2, radius=R_M):

90 '''Compute the intersection points of two circles each defined

91 by a center point and a radius.

92

93 @arg rad1: Radius of the this circle (C{meter} or C{radians},

94 see B{C{radius}}).

95 @arg other: Center of the other circle (C{Cartesian}).

96 @arg rad2: Radius of the other circle (C{meter} or C{radians},

97 see B{C{radius}}).

98 @kwarg radius: Mean earth radius (C{meter} or C{None} if both

99 B{C{rad1}} and B{C{rad2}} are given in C{radians}).

100

101 @return: 2-Tuple of the intersection points, each C{Cartesian}.

102 For abutting circles, the intersection points are the

103 same C{Cartesian} instance, aka the I{radical center}.

104

105 @raise IntersectionError: Concentric, antipodal, invalid or

106 non-intersecting circles.

107

108 @raise TypeError: If B{C{other}} is not C{Cartesian}.

109

110 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}} or B{C{radius}}.

111

112 @see: U{Calculating intersection of two Circles

113 <https://GIS.StackExchange.com/questions/48937/

114 calculating-intersection-of-two-circles>} and method

115 or function C{trilaterate3d2}.

116 '''

117 x1, x2 = self, self.others(other)

118 r1, r2, x = _rads3(rad1, rad2, radius)

119 if x:

120 x1, x2 = x2, x1

121 try:

122 n, q = x1.cross(x2), x1.dot(x2)

123 n2, q1 = n.length2, (_1_0 - q**2)

124 if n2 < EPS or isnear0(q1):

125 raise ValueError(_near_(_concentric_))

126 c1, c2 = cos(r1), cos(r2)

127 x0 = x1.times((c1 - q * c2) / q1).plus(

128 x2.times((c2 - q * c1) / q1))

129 n1 = _1_0 - x0.length2

130 if n1 < EPS:

131 raise ValueError(_too_(_distant_))

132 except ValueError as x:

133 raise IntersectionError(center=self, rad1=rad1,

134 other=other, rad2=rad2, cause=x)

135 n = n.times(sqrt(n1 / n2))

136 if n.length > EPS:

137 x1 = x0.plus(n)

138 x2 = x0.minus(n)

139 else: # abutting circles

140 x1 = x2 = x0

141

142 return (_xattrs(x1, self, _datum_, _name_),

143 _xattrs(x2, self, _datum_, _name_))

144

145

146 class LatLonSphericalBase(LatLonBase):

147 '''(INTERNAL) Base class for spherical C{LatLon}s.

148 '''

149 _datum = Datums.Sphere # spherical L{Datum}

150

151 def __init__(self, latlonh, lon=None, height=0, datum=None, wrap=False, name=NN):

152 '''Create a spherical C{LatLon} point frome the given lat-, longitude and

153 height on the given datum.

154

155 @arg latlonh: Latitude (C{degrees} or DMS C{str} with N or S suffix) or

156 a previous C{LatLon} instance provided C{B{lon}=None}.

157 @kwarg lon: Longitude (C{degrees} or DMS C{str} with E or W suffix) or

158 C(None), indicating B{C{latlonh}} is a C{LatLon}.

159 @kwarg height: Optional height above (or below) the earth surface (C{meter},

160 same units as the datum's ellipsoid axes or radius).

161 @kwarg datum: Optional, spherical datum to use (L{Datum}, L{Ellipsoid},

162 L{Ellipsoid2}, L{a_f2Tuple}) or earth radius in C{meter},

163 conventionally).

164 @kwarg wrap: If C{True}, wrap or I{normalize} B{C{lat}} and B{C{lon}}

165 (C{bool}).

166 @kwarg name: Optional name (string).

167

168 @raise TypeError: If B{C{latlonh}} is not a C{LatLon} or B{C{datum}} not

169 spherical.

170 '''

171 LatLonBase.__init__(self, latlonh, lon=lon, height=height, wrap=wrap, name=name)

172 if datum not in (None, self.datum):

173 self.datum = datum

174

175 def bearingTo2(self, other, wrap=False, raiser=False):

176 '''Return the initial and final bearing (forward and reverse

177 azimuth) from this to an other point.

178

179 @arg other: The other point (C{LatLon}).

180 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the

181 B{C{other}} point (C{bool}).

182

183 @return: A L{Bearing2Tuple}C{(initial, final)}.

184

185 @raise TypeError: The B{C{other}} point is not spherical.

186

187 @see: Methods C{initialBearingTo} and C{finalBearingTo}.

188 '''

189 # .initialBearingTo is inside .-Nvector and .-Trigonometry

190 i = self.initialBearingTo(other, wrap=wrap, raiser=raiser) # PYCHOK .initialBearingTo

191 f = self.finalBearingTo( other, wrap=wrap, raiser=raiser)

192 return Bearing2Tuple(i, f, name=self.name)

193

194 @property_doc_(''' this point's datum (L{Datum}).''')

195 def datum(self):

196 '''Get this point's datum (L{Datum}).

197 '''

198 return self._datum

199

200 @datum.setter # PYCHOK setter!

201 def datum(self, datum):

202 '''Set this point's datum I{without conversion} (L{Datum}, L{Ellipsoid},

203 L{Ellipsoid2}, L{a_f2Tuple}) or C{scalar} spherical earth radius).

204

205 @raise TypeError: If B{C{datum}} invalid or not not spherical.

206 '''

207 d = _spherical_datum(datum, name=self.name, raiser=_datum_)

208 if self._datum != d:

209 _update_all(self)

210 self._datum = d

211

212 def finalBearingTo(self, other, wrap=False, raiser=False):

213 '''Return the final bearing (reverse azimuth) from this to

214 an other point.

215

216 @arg other: The other point (spherical C{LatLon}).

217 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll

218 the B{C{other}} point (C{bool}).

219

220 @return: Final bearing (compass C{degrees360}).

221

222 @raise TypeError: The B{C{other}} point is not spherical.

223

224 @example:

225

226 >>> p = LatLon(52.205, 0.119)

227 >>> q = LatLon(48.857, 2.351)

228 >>> b = p.finalBearingTo(q) # 157.9

229 '''

230 p = self.others(other)

231 if wrap:

232 p = _unrollon(self, p, wrap=wrap)

233 # final bearing is the reverse of the other, initial one;

234 # .initialBearingTo is inside .-Nvector and .-Trigonometry

235 b = p.initialBearingTo(self, wrap=False, raiser=raiser)

236 return _umod_360(b + _180_0)

237

238 def intersecant2(self, circle, point, bearing, radius=R_M, exact=False,

239 height=None, wrap=False): # was=True

240 '''Compute the intersections of a circle and a line given as a point

241 and bearing or as two points.

242

243 @arg circle: Radius of the circle centered at this location

244 (C{meter}, same units as B{C{radius}}) or a point

245 on the circle (this C{LatLon}).

246 @arg point: An other point in- or outside the circle on the line

247 (this C{LatLon}).

248 @arg bearing: Bearing at the B{C{point}} (compass C{degrees360})

249 or an other point on the line (this C{LatLon}).

250 @kwarg radius: Mean earth radius (C{meter}, conventionally).

251 @kwarg exact: If C{True} use the I{exact} rhumb methods for azimuth,

252 destination and distance, if C{False} use the basic

253 rhumb methods (C{bool}) or if C{None} use the I{great

254 circle} methods.

255 @kwarg height: Optional height for the intersection points (C{meter},

256 conventionally) or C{None}.

257 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the

258 B{C{point}}, B{C{circle}} and/or B{C{bearing}} (C{bool}).

259

260 @return: 2-Tuple of the intersection points (representing a chord),

261 each an instance of this class. For a tangent line, both

262 points are the same instance, the B{C{point}} or wrapped

263 or I{normalized}.

264

265 @raise IntersectionError: The circle and line do not intersect.

266

267 @raise TypeError: If B{C{point}} is not this C{LatLon} or B{C{circle}}

268 or B{C{bearing}} invalid.

269

270 @raise ValueError: Invalid B{C{circle}}, B{C{bearing}}, B{C{radius}},

271 B{C{exact}} or B{C{height}}.

272 '''

273 p = self.others(point=point)

274 try:

275 return _intersecant2(self, circle, p, bearing, radius=radius, exact=exact,

276 height=height, wrap=wrap)

277 except (TypeError, ValueError) as x:

278 raise _xError(x, center=self, circle=circle, point=point, bearing=bearing,

279 exact=exact, wrap=wrap)

280

281 def maxLat(self, bearing):

282 '''Return the maximum latitude reached when travelling

283 on a great circle on given bearing from this point

284 based on Clairaut's formula.

285

286 The maximum latitude is independent of longitude

287 and the same for all points on a given latitude.

288

289 Negate the result for the minimum latitude (on the

290 Southern hemisphere).

291

292 @arg bearing: Initial bearing (compass C{degrees360}).

293

294 @return: Maximum latitude (C{degrees90}).

295

296 @raise ValueError: Invalid B{C{bearing}}.

297 '''

298 m = acos1(fabs(sin(Bearing_(bearing)) * cos(self.phi)))

299 return degrees90(m)

300

301 def minLat(self, bearing):

302 '''Return the minimum latitude reached when travelling

303 on a great circle on given bearing from this point.

304

305 @arg bearing: Initial bearing (compass C{degrees360}).

306

307 @return: Minimum latitude (C{degrees90}).

308

309 @see: Method L{maxLat} for more details.

310

311 @raise ValueError: Invalid B{C{bearing}}.

312 '''

313 return -self.maxLat(bearing)

314

315 def parse(self, strllh, height=0, sep=_COMMA_, name=NN):

316 '''Parse a string representing a similar, spherical C{LatLon}

317 point, consisting of C{"lat, lon[, height]"}.

318

319 @arg strllh: Lat, lon and optional height (C{str}),

320 see function L{pygeodesy.parse3llh}.

321 @kwarg height: Optional, default height (C{meter}).

322 @kwarg sep: Optional separator (C{str}).

323 @kwarg name: Optional instance name (C{str}),

324 overriding this name.

325

326 @return: The similar point (spherical C{LatLon}).

327

328 @raise ParseError: Invalid B{C{strllh}}.

329 '''

330 t = _MODS.dms.parse3llh(strllh, height=height, sep=sep)

331 r = self.classof(*t)

332 if name:

333 r.rename(name)

334 return r

335

336 @property_RO

337 def _radius(self):

338 '''(INTERNAL) Get this sphere's radius.

339 '''

340 return self.datum.ellipsoid.equatoradius

341

342 def _rhumbs3(self, other, wrap, r=False): # != .latlonBase._rhumbx3

343 '''(INTERNAL) Rhumb_ helper function.

344

345 @arg other: The other point (spherical C{LatLon}).

346 '''

347 p = self.others(other, up=2)

348 if wrap:

349 p = _unrollon(self, p, wrap=wrap)

350 a2, b2 = p.philam

351 a1, b1 = self.philam

352 # if |db| > 180 take shorter rhumb

353 # line across the anti-meridian

354 db = wrapPI(b2 - b1)

355 dp = _logPI_2_2(a2, a1)

356 da = a2 - a1

357 if r:

358 # on Mercator projection, longitude distances shrink

359 # by latitude; the 'stretch factor' q becomes ill-

360 # conditioned along E-W line (0/0); use an empirical

361 # tolerance to avoid it

362 q = (da / dp) if fabs(dp) > EPS else cos(a1)

363 da = hypot(da, q * db) # angular distance radians

364 return da, db, dp

365

366 def rhumbAzimuthTo(self, other, radius=R_M, exact=False, wrap=False):

367 '''Return the azimuth (bearing) of a rhumb line (loxodrome)

368 between this and an other (spherical) point.

369

370 @arg other: The other point (spherical C{LatLon}).

371 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},

372 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).

373 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),

374 default C{False} for backward compatibility.

375 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the

376 B{C{other}} point (C{bool}).

377

378 @return: Rhumb line azimuth (compass C{degrees180}).

379

380 @raise TypeError: The B{C{other}} point is incompatible or

381 B{C{radius}} is invalid.

382

383 @example:

384

385 >>> p = LatLon(51.127, 1.338)

386 >>> q = LatLon(50.964, 1.853)

387 >>> b = p.rhumbBearingTo(q) # 116.7

388 '''

389 if exact: # use series, always

390 z = LatLonBase.rhumbAzimuthTo(self, other, exact=False, # Krüger

391 radius=radius, wrap=wrap)

392 else:

393 _, db, dp = self._rhumbs3(other, wrap)

394 z = atan2d(db, dp) # see .rhumbx.Rhumb.Inverse

395 return z

396

397 @deprecated_method

398 def rhumbBearingTo(self, other): # unwrapped

399 '''DEPRECATED, use method C{.rhumbAzimuthTo}.'''

400 return wrap360(self.rhumbAzimuthTo(other)) # [0..360)

401

402 def rhumbDestination(self, distance, bearing, radius=R_M, height=None, exact=False):

403 '''Return the destination point having travelled the given distance

404 from this point along a rhumb line (loxodrome) at the given bearing.

405

406 @arg distance: Distance travelled (C{meter}, same units as B{C{radius}}),

407 may be negative if C{B{exact}=True}.

408 @arg bearing: Bearing (azimuth) at this point (compass C{degrees360}).

409 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},

410 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}) if

411 C{B{exact}=True}.

412 @kwarg height: Optional height, overriding the default height

413 (C{meter}, same unit as B{C{radius}}).

414 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),

415 default C{False} for backward compatibility.

416

417 @return: The destination point (spherical C{LatLon}).

418

419 @raise ValueError: Invalid B{C{distance}}, B{C{bearing}},

420 B{C{radius}} or B{C{height}}.

421

422 @example:

423

424 >>> p = LatLon(51.127, 1.338)

425 >>> q = p.rhumbDestination(40300, 116.7) # 50.9642°N, 001.8530°E

426 '''

427 if exact: # use series, always

428 r = LatLonBase.rhumbDestination(self, distance, bearing, exact=False, # Krüger

429 radius=radius, height=height)

430 else: # radius=None from .rhumbMidpointTo

431 if radius in (None, self._radius):

432 d, r = self.datum, radius

433 else:

434 d = _spherical_datum(radius, raiser=_radius_) # spherical only

435 r = d.ellipsoid.equatoradius

436 r = _angular(distance, r, low=-EPS) # distance=0 from .rhumbMidpointTo

437

438 a1, b1 = self.philam

439 sb, cb = sincos2(Bearing_(bearing))

440

441 da = r * cb

442 a2 = a1 + da

443 # normalize latitude if past pole

444 if fabs(a2) > PI_2:

445 a2 = _copysign(PI, a2) - a2

446

447 dp = _logPI_2_2(a2, a1)

448 # q becomes ill-conditioned on E-W course 0/0

449 q = (da / dp) if fabs(dp) > EPS else cos(a1)

450 b2 = (b1 + r * sb / q) if fabs(q) > EPS else b1

451

452 h = self._heigHt(height)

453 r = self.classof(degrees90(a2), degrees180(b2), datum=d, height=h)

454 return r

455

456 def rhumbDistanceTo(self, other, radius=R_M, exact=False, wrap=False):

457 '''Return the distance from this to an other point along

458 a rhumb line (loxodrome).

459

460 @arg other: The other point (spherical C{LatLon}).

461 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},

462 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}) if

463 C{B{exact}=True}.

464 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),

465 default C{False} for backward compatibility.

466 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the

467 B{C{other}} point (C{bool}).

468

469 @return: Distance (C{meter}, the same units as B{C{radius}}

470 or C{radians} if B{C{radius}} is C{None}).

471

472 @raise TypeError: The B{C{other}} point is incompatible.

473

474 @raise ValueError: Invalid B{C{radius}}.

475

476 @example:

477

478 >>> p = LatLon(51.127, 1.338)

479 >>> q = LatLon(50.964, 1.853)

480 >>> d = p.rhumbDistanceTo(q) # 403100

481 '''

482 if exact: # use series, always

483 r = LatLonBase.rhumbDistanceTo(self, other, exact=False, # Krüger

484 radius=radius, wrap=wrap)

485 if radius is None: # angular distance in radians

486 r = r / self._radius # /= chokes PyChecker

487 else:

488 # see <https://www.EdWilliams.org/avform.htm#Rhumb>

489 r, _, _ = self._rhumbs3(other, wrap, r=True)

490 if radius is not None:

491 r *= Radius(radius)

492 return r

493

494 def rhumbMidpointTo(self, other, height=None, radius=R_M, exact=False,

495 fraction=_0_5, wrap=False):

496 '''Return the (loxodromic) midpoint on the rhumb line between

497 this and an other point.

498

499 @arg other: The other point (spherical LatLon).

500 @kwarg height: Optional height, overriding the mean height

501 (C{meter}).

502 @kwarg radius: Earth radius (C{meter}) or earth model (L{Datum},

503 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).

504 @kwarg exact: If C{True}, use I{Krüger} L{rhumbx} (C{bool}),

505 default C{False} for backward compatibility.

506 @kwarg fraction: Midpoint location from this point (C{scalar}),

507 may be negative if C{B{exact}=True}.

508 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the

509 B{C{other}} point (C{bool}).

510

511 @return: The (mid)point at the given B{C{fraction}} along

512 the rhumb line (spherical C{LatLon}).

513

514 @raise TypeError: The B{C{other}} point is incompatible.

515

516 @raise ValueError: Invalid B{C{height}} or B{C{fraction}}

517

518 @example:

519

520 >>> p = LatLon(51.127, 1.338)

521 >>> q = LatLon(50.964, 1.853)

522 >>> m = p.rhumb_midpointTo(q)

523 >>> m.toStr() # '51.0455°N, 001.5957°E'

524 '''

525 if exact: # use series, always

526 r = LatLonBase.rhumbMidpointTo(self, other, exact=False, # Krüger

527 radius=radius, height=height,

528 fraction=fraction, wrap=wrap)

529 elif fraction is not _0_5:

530 f = Scalar_(fraction=fraction) # low=_0_0

531 r, db, dp = self._rhumbs3(other, wrap, r=True) # radians

532 z = atan2b(db, dp)

533 h = self._havg(other, f=f, h=height)

534 r = self.rhumbDestination(r * f, z, radius=None, height=h)

535

536 else: # for backward compatibility, unwrapped

537 # see <https://MathForum.org/library/drmath/view/51822.html>

538 a1, b1 = self.philam

539 a2, b2 = self.others(other).philam

540

541 if fabs(b2 - b1) > PI:

542 b1 += PI2 # crossing anti-meridian

543

544 a3 = favg(a1, a2)

545 b3 = favg(b1, b2)

546

547 f1 = tanPI_2_2(a1)

548 if isnon0(f1):

549 f2 = tanPI_2_2(a2)

550 f = f2 / f1

551 if isnon0(f):

552 f = log(f)

553 if isnon0(f):

554 f3 = tanPI_2_2(a3)

555 b3 = fdot(map1(log, f1, f2, f3),

556 -b2, b1, b2 - b1) / f

557

558 d = self.datum if radius in (None, self._radius) else \

559 _spherical_datum(radius, name=self.name, raiser=_radius_)

560 h = self._havg(other, h=height)

561 r = self.classof(degrees90(a3), degrees180(b3), datum=d, height=h)

562 return r

563

564 def toNvector(self, Nvector=NvectorBase, **Nvector_kwds): # PYCHOK signature

565 '''Convert this point to C{Nvector} components, I{including

566 height}.

567

568 @kwarg Nvector_kwds: Optional, additional B{C{Nvector}}

569 keyword arguments, ignored if

570 C{B{Nvector} is None}.

571

572 @return: An B{C{Nvector}} or a L{Vector4Tuple}C{(x, y, z, h)}

573 if B{C{Nvector}} is C{None}.

574

575 @raise TypeError: Invalid B{C{Nvector}} or B{C{Nvector_kwds}}.

576 '''

577 return LatLonBase.toNvector(self, Nvector=Nvector, **Nvector_kwds)

578

579

580 def _intersecant2(c, r, p, b, radius=R_M, exact=False,

581 height=None, wrap=False):

582 # (INTERNAL) Intersect a circle and bearing, see L{intersecant2}

583 # above, separated to allow callers to embellish any exceptions

584

585 if wrap:

586 p = _unrollon(c, p, wrap=wrap)

587 nonexact = exact is None

588

589 if not isinstanceof(r, c.__class__, p.__class__):

590 r = Radius_(circle=r)

591 elif nonexact:

592 r = c.distanceTo(r, radius=radius, wrap=wrap)

593 elif isbool(exact):

594 r = c.rhumbDistanceTo(r, radius=radius, exact=exact, wrap=wrap)

595 else:

596 raise _ValueError(exact=exact)

597

598 if not isinstanceof(b, c.__class__, p.__class__):

599 b = Bearing(b)

600 elif nonexact:

601 b = p.initialBearingTo(b, wrap=wrap)

602 else:

603 b = p.rhumbAzimuthTo(b, radius=radius, exact=exact, wrap=wrap)

604

605 d = p.distanceTo(c, radius=radius) if nonexact else \

606 p.rhumbDistanceTo(c, radius=radius, exact=exact)

607 a = 0 if d < EPS else (p.initialBearingTo(c) if nonexact else

608 wrap360(p.rhumbAzimuthTo(c, radius=radius, exact=exact)))

609 return _intersecend2(p, d, a, b, r, radius, height, exact)

610

611

612 def _r2m(r, radius):

613 '''(INTERNAL) Angular distance in C{radians} to C{meter}.

614 '''

615 if radius is not None: # not in (None, _0_0)

616 r *= R_M if radius is R_M else Radius(radius)

617 return r

618

619

620__all__ += _ALL_DOCS(CartesianSphericalBase, LatLonSphericalBase)

621

622# **) MIT License

623#

624# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

625#

626# Permission is hereby granted, free of charge, to any person obtaining a

627# copy of this software and associated documentation files (the "Software"),

628# to deal in the Software without restriction, including without limitation

629# the rights to use, copy, modify, merge, publish, distribute, sublicense,

630# and/or sell copies of the Software, and to permit persons to whom the

631# Software is furnished to do so, subject to the following conditions:

632#

633# The above copyright notice and this permission notice shall be included

634# in all copies or substantial portions of the Software.

635#

636# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

637# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

638# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

639# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

640# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

641# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

642# OTHER DEALINGS IN THE SOFTWARE.