Coverage for pygeodesy/sphericalTrigonometry.py: 93%

1

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

3

4 u'''Spherical, C{trigonometry}-based geodesy.

5

6 Trigonometric classes geodetic (lat-/longitude) L{LatLon} and

7 geocentric (ECEF) L{Cartesian} and functions L{areaOf}, L{intersection},

8 L{intersections2}, L{isPoleEnclosedBy}, L{meanOf}, L{nearestOn3} and

9 L{perimeterOf}, I{all spherical}.

10

11 Pure Python implementation of geodetic (lat-/longitude) methods using

12 spherical trigonometry, transcoded from JavaScript originals by

13 I{(C) Chris Veness 2011-2024} published under the same MIT Licence**, see

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

15'''

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

17 from __future__ import division as _; del _ # noqa: E702 ;

18

19 from pygeodesy.basics import copysign0, _isin, map1, signOf, typename

20 from pygeodesy.constants import EPS, EPS1, EPS4, PI, PI2, PI_2, PI_4, \

21 R_M, _0_0, _0_5, _1_0, _2_0, _90_0, \

22 isnear0, isnear1, isnon0

23 from pygeodesy.datums import _ellipsoidal_datum, _mean_radius

24 from pygeodesy.errors import _AssertionError, CrossError, crosserrors, \

25 _TypeError, _ValueError, IntersectionError, \

26 _xError, _xkwds, _xkwds_get, _xkwds_pop2

27 from pygeodesy.fmath import favg, fdot, fdot_, fmean, hypot

28 from pygeodesy.fsums import Fsum, fsum, fsumf_

29 from pygeodesy.formy import antipode_, bearing_, _bearingTo2, excessAbc_, \

30 excessGirard_, excessLHuilier_, opposing_, _radical2, \

31 vincentys_

32# from pygeodesy.internals import typename # from .basics

33 from pygeodesy.interns import _1_, _2_, _coincident_, _composite_, _colinear_, \

34 _concentric_, _convex_, _end_, _infinite_, \

35 _invalid_, _line_, _near_, _null_, _parallel_, \

36 _point_, _SPACE_, _too_

37 from pygeodesy.latlonBase import _trilaterate5

38 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _ALL_OTHER

39# from pygeodesy.nvectorBase import _N_Vector, NvectorBase, sumOf # _MODS

40 from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, NearestOn3Tuple, \

41 Triangle7Tuple, Triangle8Tuple

42 from pygeodesy.points import ispolar, nearestOn5 as _nearestOn5, \

43 Fmt as _Fmt # XXX shadowed

44 from pygeodesy.props import deprecated_function, deprecated_method

45 from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \

46 _intersecant2, LatLonSphericalBase, \

47 _rads3, _radians2m

48# from pygeodesy.streprs import Fmt as _Fmt # from .points XXX shadowed

49 from pygeodesy.units import Bearing_, Height, _isDegrees, _isRadius, Lamd, \

50 Phid, Radius_, Scalar

51 from pygeodesy.utily import acos1, asin1, atan1d, atan2, atan2d, degrees90, \

52 degrees180, degrees2m, m2radians, radiansPI2, \

53 sincos2_, tan_2, unrollPI, _unrollon, _unrollon3, \

54 wrap180, wrapPI, _Wrap

55 from pygeodesy.vector3d import sumOf, Vector3d

56

57 from math import asin, cos, degrees, fabs, radians, sin

58

59 __all__ = _ALL_LAZY.sphericalTrigonometry

60 __version__ = '25.08.31'

61

62 _PI_EPS4 = PI - EPS4

63 if _PI_EPS4 >= PI:

64 raise _AssertionError(EPS4=EPS4, PI=PI, PI_EPS4=_PI_EPS4)

65

66

67 class Cartesian(CartesianSphericalBase):

68 '''Extended to convert geocentric, L{Cartesian} points to

69 spherical, geodetic L{LatLon}.

70 '''

71

72 def toLatLon(self, **LatLon_and_kwds): # PYCHOK LatLon=LatLon

73 '''Convert this cartesian point to a C{spherical} geodetic point.

74

75 @kwarg LatLon_and_kwds: Optional L{LatLon} and L{LatLon} keyword

76 arguments. Use C{B{LatLon}=...} to override

77 this L{LatLon} class or specify C{B{LatLon}=None}.

78

79 @return: The geodetic point (L{LatLon}) or if C{B{LatLon} is None},

80 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}

81 with C{C} and C{M} if available.

82

83 @raise TypeError: Invalid B{C{LatLon_and_kwds}} argument.

84 '''

85 kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)

86 return CartesianSphericalBase.toLatLon(self, **kwds)

87

88

89 class LatLon(LatLonSphericalBase):

90 '''New point on a spherical earth model, based on trigonometry formulae.

91 '''

92

93 def _ab1_ab2_db5(self, other, wrap):

94 '''(INTERNAL) Helper for several methods.

95 '''

96 a1, b1 = self.philam

97 a2, b2 = self.others(other, up=2).philam

98 if wrap:

99 a2, b2 = _Wrap.philam(a2, b2)

100 db, b2 = unrollPI(b1, b2, wrap=wrap)

101 else: # unrollPI shortcut

102 db = b2 - b1

103 return a1, b1, a2, b2, db

104

105 def alongTrackDistanceTo(self, start, end, radius=R_M, wrap=False):

106 '''Compute the (signed) distance from the start to the closest

107 point on the great circle line defined by a start and an

108 end point.

109

110 That is, if a perpendicular is drawn from this point to the

111 great circle line, the along-track distance is the distance

112 from the start point to the point where the perpendicular

113 crosses the line.

114

115 @arg start: Start point of the great circle line (L{LatLon}).

116 @arg end: End point of the great circle line (L{LatLon}).

117 @kwarg radius: Mean earth radius (C{meter}) or C{None}.

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

119 the B{C{start}} and B{C{end}} point (C{bool}).

120

121 @return: Distance along the great circle line (C{radians}

122 if C{B{radius} is None} or C{meter}, same units

123 as B{C{radius}}), positive if I{after} the

124 B{C{start}} toward the B{C{end}} point of the

125 line, I{negative} if before or C{0} if at the

126 B{C{start}} point.

127

128 @raise TypeError: Invalid B{C{start}} or B{C{end}} point.

129

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

131 '''

132 r, x, b = self._a_x_b3(start, end, radius, wrap)

133 cx = cos(x)

134 return _0_0 if isnear0(cx) else \

135 _radians2m(copysign0(acos1(cos(r) / cx), cos(b)), radius)

136

137 def _a_x_b3(self, start, end, radius, wrap):

138 '''(INTERNAL) Helper for .along-/crossTrackDistanceTo.

139 '''

140 s = self.others(start=start)

141 e = self.others(end=end)

142 s, e, w = _unrollon3(self, s, e, wrap)

143

144 r = Radius_(radius)

145 r = s.distanceTo(self, r, wrap=w) / r

146

147 b = radians(s.initialBearingTo(self, wrap=w)

148 - s.initialBearingTo(e, wrap=w))

149 x = asin(sin(r) * sin(b))

150 return r, x, -b

151

152 @deprecated_method

153 def bearingTo(self, other, wrap=False, raiser=False): # PYCHOK no cover

154 '''DEPRECATED, use method L{initialBearingTo}.

155 '''

156 return self.initialBearingTo(other, wrap=wrap, raiser=raiser)

157

158 def crossingParallels(self, other, lat, wrap=False):

159 '''Return the pair of meridians at which a great circle defined

160 by this and an other point crosses the given latitude.

161

162 @arg other: The other point defining great circle (L{LatLon}).

163 @arg lat: Latitude at the crossing (C{degrees}).

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

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

166

167 @return: 2-Tuple C{(lon1, lon2)}, both in C{degrees180} or

168 C{None} if the great circle doesn't reach B{C{lat}}.

169 '''

170 a1, b1, a2, b2, db = self._ab1_ab2_db5(other, wrap)

171 sa, ca, sa1, ca1, \

172 sa2, ca2, sdb, cdb = sincos2_(radians(lat), a1, a2, db)

173 sa1 *= ca2 * ca

174

175 x = sa1 * sdb

176 y = sa1 * cdb - ca1 * sa2 * ca

177 z = ca1 * sdb * ca2 * sa

178

179 h = hypot(x, y)

180 if h < EPS or fabs(z) > h: # PYCHOK no cover

181 return None # great circle doesn't reach latitude

182

183 m = atan2(-y, x) + b1 # longitude at max latitude

184 d = acos1(z / h) # delta longitude to intersections

185 return degrees180(m - d), degrees180(m + d)

186

187 def crossTrackDistanceTo(self, start, end, radius=R_M, wrap=False):

188 '''Compute the (signed) distance from this point to a great

189 circle from a start to an end point.

190

191 @arg start: Start point of the great circle line (L{LatLon}).

192 @arg end: End point of the great circle line (L{LatLon}).

193 @kwarg radius: Mean earth radius (C{meter}) or C{None}.

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

195 the B{C{start}} and B{C{end}} point (C{bool}).

196

197 @return: Distance to the great circle (C{radians} if

198 B{C{radius}} or C{meter}, same units as

199 B{C{radius}}), I{negative} if to the left or

200 I{positive} if to the right of the line.

201

202 @raise TypeError: If B{C{start}} or B{C{end}} is not L{LatLon}.

203

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

205 '''

206 _, x, _ = self._a_x_b3(start, end, radius, wrap)

207 return _radians2m(x, radius)

208

209 def destination(self, distance, bearing, radius=R_M, height=None):

210 '''Locate the destination from this point after having

211 travelled the given distance on a bearing from North.

212

213 @arg distance: Distance travelled (C{meter}, same units as

214 B{C{radius}}).

215 @arg bearing: Bearing from this point (compass C{degrees360}).

216 @kwarg radius: Mean earth radius (C{meter}).

217 @kwarg height: Optional height at destination (C{meter}, same

218 units a B{C{radius}}).

219

220 @return: Destination point (L{LatLon}).

221

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

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

224 '''

225 a, b = self.philam

226 r, t = _m2radians(distance, radius, low=None), Bearing_(bearing)

227

228 a, b = _destination2(a, b, r, t)

229 h = self._heigHt(height)

230 return self.classof(degrees90(a), degrees180(b), height=h)

231

232 def distanceTo(self, other, radius=R_M, wrap=False):

233 '''Compute the (angular) distance from this to an other point.

234

235 @arg other: The other point (L{LatLon}).

236 @kwarg radius: Mean earth radius (C{meter}) or C{None}.

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

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

239

240 @return: Distance between this and the B{C{other}} point

241 (C{meter}, same units as B{C{radius}} or

242 C{radians} if C{B{radius} is None}).

243

244 @raise TypeError: The B{C{other}} point is not L{LatLon}.

245

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

247 '''

248 a1, _, a2, _, db = self._ab1_ab2_db5(other, wrap)

249 return _radians2m(vincentys_(a2, a1, db), radius)

250

251# @Property_RO

252# def Ecef(self):

253# '''Get the ECEF I{class} (L{EcefVeness}), I{lazily}.

254# '''

255# return _MODS.ecef.EcefKarney

256

257 def greatCircle(self, bearing, Vector=Vector3d, **Vector_kwds):

258 '''Compute the vector normal to great circle obtained by heading

259 from this point on the bearing from North.

260

261 Direction of vector is such that initial bearing vector

262 b = c × n, where n is an n-vector representing this point.

263

264 @arg bearing: Bearing from this point (compass C{degrees360}).

265 @kwarg Vector: Vector class to return the great circle,

266 overriding the default L{Vector3d}.

267 @kwarg Vector_kwds: Optional, additional keyword argunents

268 for B{C{Vector}}.

269

270 @return: Vector representing great circle (C{Vector}).

271

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

273 '''

274 a, b = self.philam

275 sa, ca, sb, cb, st, ct = sincos2_(a, b, Bearing_(bearing))

276

277 sa *= st

278 return Vector(fdot_(sb, ct, -cb, sa),

279 -fdot_(cb, ct, sb, sa),

280 ca * st, **Vector_kwds) # XXX .unit()?

281

282 def initialBearingTo(self, other, wrap=False, raiser=False):

283 '''Compute the initial bearing (forward azimuth) from this

284 to an other point.

285

286 @arg other: The other point (spherical L{LatLon}).

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

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

289 @kwarg raiser: Optionally, raise L{CrossError} (C{bool}),

290 use C{B{raiser}=True} for behavior like

291 C{sphericalNvector.LatLon.initialBearingTo}.

292

293 @return: Initial bearing (compass C{degrees360}).

294

295 @raise CrossError: If this and the B{C{other}} point coincide

296 and if B{C{raiser}} and L{crosserrors

297 <pygeodesy.crosserrors>} are both C{True}.

298

299 @raise TypeError: The B{C{other}} point is not L{LatLon}.

300 '''

301 a1, b1, a2, b2, db = self._ab1_ab2_db5(other, wrap)

302 # XXX behavior like sphericalNvector.LatLon.initialBearingTo

303 if raiser and crosserrors() and max(fabs(a2 - a1), fabs(db)) < EPS:

304 raise CrossError(_point_, self, other=other, wrap=wrap, txt=_coincident_)

305

306 return degrees(bearing_(a1, b1, a2, b2, final=False))

307

308 def intermediateTo(self, other, fraction, height=None, wrap=False):

309 '''Locate the point at given fraction between (or along) this

310 and an other point.

311

312 @arg other: The other point (L{LatLon}).

313 @arg fraction: Fraction between both points (C{scalar},

314 0.0 at this and 1.0 at the other point).

315 @kwarg height: Optional height, overriding the intermediate

316 height (C{meter}).

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

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

319

320 @return: Intermediate point (L{LatLon}).

321

322 @raise TypeError: The B{C{other}} point is not L{LatLon}.

323

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

325

326 @see: Methods C{midpointTo} and C{rhumbMidpointTo}.

327 '''

328 p = self

329 f = Scalar(fraction=fraction)

330 if not isnear0(f):

331 p = p.others(other)

332 if wrap:

333 p = _Wrap.point(p)

334 if not isnear1(f): # and not near0

335 a1, b1 = self.philam

336 a2, b2 = p.philam

337 db, b2 = unrollPI(b1, b2, wrap=wrap)

338 r = vincentys_(a2, a1, db)

339 sr = sin(r)

340 if isnon0(sr):

341 sa1, ca1, sa2, ca2, \

342 sb1, cb1, sb2, cb2 = sincos2_(a1, a2, b1, b2)

343

344 t = f * r

345 a = sin(r - t) # / sr superflous

346 b = sin( t) # / sr superflous

347

348 x = fdot_(a, ca1 * cb1, b, ca2 * cb2)

349 y = fdot_(a, ca1 * sb1, b, ca2 * sb2)

350 z = fdot_(a, sa1, b, sa2)

351

352 a = atan1d(z, hypot(x, y))

353 b = atan2d(y, x)

354

355 else: # PYCHOK no cover

356 a = degrees90( favg(a1, a2, f=f)) # coincident

357 b = degrees180(favg(b1, b2, f=f))

358

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

360 p = self.classof(a, b, height=h)

361 return p

362

363 def intersection(self, end1, other, end2, height=None, wrap=False):

364 '''Compute the intersection point of two lines, each defined by

365 two points or a start point and a bearing from North.

366

367 @arg end1: End point of this line (L{LatLon}) or the initial

368 bearing at this point (compass C{degrees360}).

369 @arg other: Start point of the other line (L{LatLon}).

370 @arg end2: End point of the other line (L{LatLon}) or the

371 initial bearing at the B{C{other}} point (compass

372 C{degrees360}).

373 @kwarg height: Optional height for intersection point,

374 overriding the mean height (C{meter}).

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

376 B{C{start2}} and both B{C{end*}} points (C{bool}).

377

378 @return: The intersection point (L{LatLon}). An alternate

379 intersection point might be the L{antipode} to

380 the returned result.

381

382 @raise IntersectionError: Ambiguous or infinite intersection

383 or colinear, parallel or otherwise

384 non-intersecting lines.

385

386 @raise TypeError: If B{C{other}} is not L{LatLon} or B{C{end1}}

387 or B{C{end2}} not C{scalar} nor L{LatLon}.

388

389 @raise ValueError: Invalid B{C{height}} or C{null} line.

390 '''

391 try:

392 s2 = self.others(other)

393 return _intersect(self, end1, s2, end2, height=height, wrap=wrap,

394 LatLon=self.classof)

395 except (TypeError, ValueError) as x:

396 raise _xError(x, start1=self, end1=end1,

397 other=other, end2=end2, wrap=wrap)

398

399 def intersections2(self, rad1, other, rad2, radius=R_M, eps=_0_0,

400 height=None, wrap=True):

401 '''Compute the intersection points of two circles, each defined

402 by a center point and a radius.

403

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

405 see B{C{radius}}).

406 @arg other: Center point of the other circle (L{LatLon}).

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

408 see B{C{radius}}).

409 @kwarg radius: Mean earth radius (C{meter} or C{None} if B{C{rad1}},

410 B{C{rad2}} and B{C{eps}} are given in C{radians}).

411 @kwarg eps: Required overlap (C{meter} or C{radians}, see

412 B{C{radius}}).

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

414 conventionally) or C{None} for the I{"radical height"}

415 at the I{radical line} between both centers.

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

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

418

419 @return: 2-Tuple of the intersection points, each a L{LatLon}

420 instance. For abutting circles, both intersection

421 points are the same instance, aka the I{radical center}.

422

423 @raise IntersectionError: Concentric, antipodal, invalid or

424 non-intersecting circles.

425

426 @raise TypeError: If B{C{other}} is not L{LatLon}.

427

428 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}}, B{C{radius}},

429 B{C{eps}} or B{C{height}}.

430 '''

431 try:

432 c2 = self.others(other)

433 return _intersects2(self, rad1, c2, rad2, radius=radius, eps=eps,

434 height=height, wrap=wrap,

435 LatLon=self.classof)

436 except (TypeError, ValueError) as x:

437 raise _xError(x, center=self, rad1=rad1,

438 other=other, rad2=rad2, wrap=wrap)

439

440 @deprecated_method

441 def isEnclosedBy(self, points): # PYCHOK no cover

442 '''DEPRECATED, use method C{isenclosedBy}.'''

443 return self.isenclosedBy(points)

444

445 def isenclosedBy(self, points, wrap=False):

446 '''Check whether a (convex) polygon or composite encloses this point.

447

448 @arg points: The polygon points or composite (L{LatLon}[],

449 L{BooleanFHP} or L{BooleanGH}).

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

451 B{C{points}} (C{bool}).

452

453 @return: C{True} if this point is inside the polygon or

454 composite, C{False} otherwise.

455

456 @raise PointsError: Insufficient number of B{C{points}}.

457

458 @raise TypeError: Some B{C{points}} are not L{LatLon}.

459

460 @raise ValueError: Invalid B{C{points}}, non-convex polygon.

461

462 @see: Functions L{pygeodesy.isconvex}, L{pygeodesy.isenclosedBy}

463 and L{pygeodesy.ispolar} especially if the B{C{points}} may

464 enclose a pole or wrap around the earth I{longitudinally}.

465 '''

466 if _MODS.booleans.isBoolean(points):

467 return points._encloses(self.lat, self.lon, wrap=wrap)

468

469 Ps = self.PointsIter(points, loop=2, dedup=True, wrap=wrap)

470 n0 = self._N_vector

471

472 v2 = Ps[0]._N_vector

473 p1 = Ps[1]

474 v1 = p1._N_vector

475 # check whether this point on same side of all

476 # polygon edges (to the left or right depending

477 # on the anti-/clockwise polygon direction)

478 gc1 = v2.cross(v1)

479 t0 = gc1.angleTo(n0) > PI_2

480 s0 = None

481 # get great-circle vector for each edge

482 for i, p2 in Ps.enumerate(closed=True):

483 if wrap and not Ps.looped:

484 p2 = _unrollon(p1, p2)

485 p1 = p2

486 v2 = p2._N_vector

487 gc = v1.cross(v2)

488 t = gc.angleTo(n0) > PI_2

489 if t != t0: # different sides of edge i

490 return False # outside

491

492 # check for convex polygon: angle between

493 # gc vectors, signed by direction of n0

494 # (otherwise the test above is not reliable)

495 s = signOf(gc1.angleTo(gc, vSign=n0))

496 if s != s0:

497 if s0 is None:

498 s0 = s

499 else:

500 t = _Fmt.SQUARE(points=i)

501 raise _ValueError(t, p2, wrap=wrap, txt_not_=_convex_)

502 gc1, v1 = gc, v2

503

504 return True # inside

505

506 def midpointTo(self, other, height=None, fraction=_0_5, wrap=False):

507 '''Find the midpoint between this and an other point.

508

509 @arg other: The other point (L{LatLon}).

510 @kwarg height: Optional height for midpoint, overriding

511 the mean height (C{meter}).

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

513 may be negative or greater than 1.0.

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

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

516

517 @return: Midpoint (L{LatLon}).

518

519 @raise TypeError: The B{C{other}} point is not L{LatLon}.

520

521 @raise ValueError: Invalid B{C{height}}.

522

523 @see: Methods C{intermediateTo} and C{rhumbMidpointTo}.

524 '''

525 if fraction is _0_5:

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

527 a1, b, a2, _, db = self._ab1_ab2_db5(other, wrap)

528 sa1, ca1, sa2, ca2, sdb, cdb = sincos2_(a1, a2, db)

529

530 x = ca2 * cdb + ca1

531 y = ca2 * sdb

532

533 a = atan1d(sa1 + sa2, hypot(x, y))

534 b = degrees180(b + atan2(y, x))

535

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

537 r = self.classof(a, b, height=h)

538 else:

539 r = self.intermediateTo(other, fraction, height=height, wrap=wrap)

540 return r

541

542 def nearestOn(self, point1, point2, radius=R_M, **wrap_adjust_limit):

543 '''Locate the point between two other points closest to this point.

544

545 Distances are approximated by function L{pygeodesy.equirectangular4},

546 subject to the supplied B{C{options}}.

547

548 @arg point1: Start point (L{LatLon}).

549 @arg point2: End point (L{LatLon}).

550 @kwarg radius: Mean earth radius (C{meter}).

551 @kwarg wrap_adjust_limit: Optional keyword arguments for functions

552 L{sphericalTrigonometry.nearestOn3} and

553 L{pygeodesy.equirectangular4},

554

555 @return: Closest point on the great circle line (L{LatLon}).

556

557 @raise LimitError: Lat- and/or longitudinal delta exceeds B{C{limit}},

558 see function L{pygeodesy.equirectangular4}.

559

560 @raise NotImplementedError: Keyword argument C{B{within}=False}

561 is not (yet) supported.

562

563 @raise TypeError: Invalid B{C{point1}} or B{C{point2}}.

564

565 @raise ValueError: Invalid B{C{radius}} or B{C{options}}.

566

567 @see: Functions L{pygeodesy.equirectangular4} and L{pygeodesy.nearestOn5}

568 and method L{sphericalTrigonometry.LatLon.nearestOn3}.

569 '''

570 # remove kwarg B{C{within}} if present

571 w, kwds = _xkwds_pop2(wrap_adjust_limit, within=True)

572 if not w:

573 self._notImplemented(within=w)

574

575# # UNTESTED - handle C{B{within}=False} and C{B{within}=True}

576# wrap = _xkwds_get(options, wrap=False)

577# a = self.alongTrackDistanceTo(point1, point2, radius=radius, wrap=wrap)

578# if fabs(a) < EPS or (within and a < EPS):

579# return point1

580# d = point1.distanceTo(point2, radius=radius, wrap=wrap)

581# if isnear0(d):

582# return point1 # or point2

583# elif fabs(d - a) < EPS or (a + EPS) > d:

584# return point2

585# f = a / d

586# if within:

587# if f > EPS1:

588# return point2

589# elif f < EPS:

590# return point1

591# return point1.intermediateTo(point2, f, wrap=wrap)

592

593 # without kwarg B{C{within}}, use backward compatible .nearestOn3

594 return self.nearestOn3([point1, point2], closed=False, radius=radius,

595 **kwds)[0]

596

597 @deprecated_method

598 def nearestOn2(self, points, closed=False, radius=R_M, **options): # PYCHOK no cover

599 '''DEPRECATED, use method L{sphericalTrigonometry.LatLon.nearestOn3}.

600

601 @return: ... 2-Tuple C{(closest, distance)} of the closest

602 point (L{LatLon}) on the polygon and the distance

603 to that point from this point in C{meter}, same

604 units of B{C{radius}}.

605 '''

606 r = self.nearestOn3(points, closed=closed, radius=radius, **options)

607 return r.closest, r.distance

608

609 def nearestOn3(self, points, closed=False, radius=R_M, **wrap_adjust_limit):

610 '''Locate the point on a polygon closest to this point.

611

612 Distances are approximated by function L{pygeodesy.equirectangular4},

613 subject to the supplied B{C{options}}.

614

615 @arg points: The polygon points (L{LatLon}[]).

616 @kwarg closed: Optionally, close the polygon (C{bool}).

617 @kwarg radius: Mean earth radius (C{meter}).

618 @kwarg wrap_adjust_limit: Optional keyword arguments for function

619 L{sphericalTrigonometry.nearestOn3} and

620 L{pygeodesy.equirectangular4},

621

622 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of the

623 C{closest} point (L{LatLon}), the L{pygeodesy.equirectangular4}

624 C{distance} between this and the C{closest} point converted to

625 C{meter}, same units as B{C{radius}}. The C{angle} from this

626 to the C{closest} point is in compass C{degrees360}, like

627 function L{pygeodesy.compassAngle}.

628

629 @raise LimitError: Lat- and/or longitudinal delta exceeds B{C{limit}},

630 see function L{pygeodesy.equirectangular4}.

631

632 @raise PointsError: Insufficient number of B{C{points}}.

633

634 @raise TypeError: Some B{C{points}} are not C{LatLon}.

635

636 @raise ValueError: Invalid B{C{radius}} or B{C{options}}.

637

638 @see: Functions L{pygeodesy.compassAngle}, L{pygeodesy.equirectangular4}

639 and L{pygeodesy.nearestOn5}.

640 '''

641 return nearestOn3(self, points, closed=closed, radius=radius,

642 LatLon=self.classof, **wrap_adjust_limit)

643

644 def toCartesian(self, **Cartesian_datum_kwds): # PYCHOK Cartesian=Cartesian, datum=None

645 '''Convert this point to C{Karney}-based cartesian (ECEF) coordinates.

646

647 @kwarg Cartesian_datum_kwds: Optional L{Cartesian}, B{C{datum}} and other

648 keyword arguments, ignored if C{B{Cartesian} is

649 None}. Use C{B{Cartesian}=...} to override this

650 L{Cartesian} class or specify C{B{Cartesian}=None}.

651

652 @return: The cartesian point (L{Cartesian}) or if C{B{Cartesian} is None},

653 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with C{C}

654 and C{M} if available.

655

656 @raise TypeError: Invalid B{C{Cartesian_datum_kwds}} argument.

657 '''

658 kwds = _xkwds(Cartesian_datum_kwds, Cartesian=Cartesian, datum=self.datum)

659 return LatLonSphericalBase.toCartesian(self, **kwds)

660

661 def triangle7(self, otherB, otherC, radius=R_M, wrap=False):

662 '''Compute the angles, sides and area of a spherical triangle.

663

664 @arg otherB: Second triangle point (C{LatLon}).

665 @arg otherC: Third triangle point (C{LatLon}).

666 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter}, L{Ellipsoid},

667 L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}) or C{None}.

668 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll points B{C{otherB}}

669 and B{C{otherC}} (C{bool}).

670

671 @return: L{Triangle7Tuple}C{(A, a, B, b, C, c, area)} or if B{C{radius} is

672 None}, a L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)}.

673

674 @see: Function L{triangle7} and U{Spherical trigonometry

675 <https://WikiPedia.org/wiki/Spherical_trigonometry>}.

676 '''

677 B = self.others(otherB=otherB)

678 C = self.others(otherC=otherC)

679 B, C, _ = _unrollon3(self, B, C, wrap)

680

681 r = self.philam + B.philam + C.philam

682 t = triangle8_(*r, wrap=wrap)

683 return self._xnamed(_t7Tuple(t, radius))

684

685 def triangulate(self, bearing1, other, bearing2, **height_wrap):

686 '''Locate a point given this, an other point and a bearing from

687 North at both points.

688

689 @arg bearing1: Bearing at this point (compass C{degrees360}).

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

691 @arg bearing2: Bearing at the other point (compass C{degrees360}).

692 @kwarg height_wrap_tol: Optional keyword arguments C{B{height}=None},

693 C{B{wrap}=False}, see method L{intersection}.

694

695 @return: Triangulated point (C{LatLon}).

696

697 @see: Method L{intersection} for further details.

698 '''

699 if _isDegrees(bearing1) and _isDegrees(bearing2):

700 return self.intersection(bearing1, other, bearing2, **height_wrap)

701 raise _TypeError(bearing1=bearing1, bearing2=bearing2, **height_wrap)

702

703 def trilaterate5(self, distance1, point2, distance2, point3, distance3,

704 area=True, eps=EPS1, radius=R_M, wrap=False):

705 '''Trilaterate three points by I{area overlap} or I{perimeter intersection}

706 of three corresponding circles.

707

708 @arg distance1: Distance to this point (C{meter}, same units as B{C{radius}}).

709 @arg point2: Second center point (C{LatLon}).

710 @arg distance2: Distance to point2 (C{meter}, same units as B{C{radius}}).

711 @arg point3: Third center point (C{LatLon}).

712 @arg distance3: Distance to point3 (C{meter}, same units as B{C{radius}}).

713 @kwarg area: If C{True}, compute the area overlap, otherwise the perimeter

714 intersection of the circles (C{bool}).

715 @kwarg eps: The required I{minimal overlap} for C{B{area}=True} or the

716 I{intersection margin} if C{B{area}=False} (C{meter}, same

717 units as B{C{radius}}).

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

719 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{point2}} and

720 B{C{point3}} (C{bool}).

721

722 @return: A L{Trilaterate5Tuple}C{(min, minPoint, max, maxPoint, n)} with

723 C{min} and C{max} in C{meter}, same units as B{C{eps}}, the

724 corresponding trilaterated points C{minPoint} and C{maxPoint}

725 as I{spherical} C{LatLon} and C{n}, the number of trilatered

726 points found for the given B{C{eps}}.

727

728 If only a single trilaterated point is found, C{min I{is} max},

729 C{minPoint I{is} maxPoint} and C{n = 1}.

730

731 For C{B{area}=True}, C{min} and C{max} are the smallest respectively

732 largest I{radial} overlap found.

733

734 For C{B{area}=False}, C{min} and C{max} represent the nearest

735 respectively farthest intersection margin.

736

737 If C{B{area}=True} and all 3 circles are concentric, C{n=0} and

738 C{minPoint} and C{maxPoint} are both the B{C{point#}} with the

739 smallest B{C{distance#}} C{min} and C{max} the largest B{C{distance#}}.

740

741 @raise IntersectionError: Trilateration failed for the given B{C{eps}},

742 insufficient overlap for C{B{area}=True} or

743 no intersection or all (near-)concentric if

744 C{B{area}=False}.

745

746 @raise TypeError: Invalid B{C{point2}} or B{C{point3}}.

747

748 @raise ValueError: Coincident B{C{point2}} or B{C{point3}} or invalid

749 B{C{distance1}}, B{C{distance2}}, B{C{distance3}}

750 or B{C{radius}}.

751 '''

752 return _trilaterate5(self, distance1,

753 self.others(point2=point2), distance2,

754 self.others(point3=point3), distance3,

755 area=area, radius=radius, eps=eps, wrap=wrap)

756

757

758 _T00 = LatLon(0, 0, name='T00') # reference instance (L{LatLon})

759

760

761 def areaOf(points, radius=R_M, wrap=False, polar=False): # was wrap=True

762 '''Calculate the area of a (spherical) polygon or composite (with the points joined by

763 great circle arcs).

764

765 @arg points: The polygon points or clips (L{LatLon}[], L{BooleanFHP} or L{BooleanGH}).

766 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter}, L{Ellipsoid},

767 L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}) or C{None}.

768 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}} (C{bool}).

769 @kwarg polar: Use C{B{polar}=True} if the polygon encloses a pole (C{bool}), see

770 function L{ispolar<pygeodesy.points.ispolar>} and U{area of a polygon

771 enclosing a pole<https://GeographicLib.SourceForge.io/C++/doc/

772 classGeographicLib_1_1GeodesicExact.html#a3d7a9155e838a09a48dc14d0c3fac525>}.

773

774 @return: Polygon area (C{meter} I{quared}, same units as B{C{radius}} or C{radians} if

775 C{B{radius} is None}).

776

777 @raise PointsError: Insufficient number of B{C{points}}.

778

779 @raise TypeError: Some B{C{points}} are not L{LatLon}.

780

781 @raise ValueError: Invalid B{C{radius}} or semi-circular polygon edge.

782

783 @note: The area is based on I{Karney}'s U{'Area of a spherical polygon'

784 <https://MathOverflow.net/questions/97711/ the-area-of-spherical-polygons>}, 3rd Answer.

785

786 @see: Functions L{pygeodesy.areaOf}, L{sphericalNvector.areaOf}, L{pygeodesy.excessKarney},

787 L{ellipsoidalExact.areaOf} and L{ellipsoidalKarney.areaOf}.

788 '''

789 if _MODS.booleans.isBoolean(points):

790 return points._sum2(LatLon, areaOf, radius=radius, wrap=wrap)

791

792 _at2, _t_2 = atan2, tan_2

793 _un, _w180 = unrollPI, wrap180

794

795 Ps = _T00.PointsIter(points, loop=1, wrap=wrap)

796 p1 = p2 = Ps[0]

797 a1, b1 = p1.philam

798 ta1, z1 = _t_2(a1), None

799

800 A = Fsum() # mean phi

801 R = Fsum() # see L{pygeodesy.excessKarney_}

802 # ispolar: Summation of course deltas around pole is 0° rather than normally ±360°

803 # <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html>

804 # XXX duplicate of function C{points.ispolar} to avoid copying all iterated points

805 D = Fsum()

806 for i, p2 in Ps.enumerate(closed=True):

807 a2, b2 = p2.philam

808 db, b2 = _un(b1, b2, wrap=wrap and not Ps.looped)

809 A += a2

810 ta2 = _t_2(a2)

811 tdb = _t_2(db, points=i)

812 R += _at2(tdb * (ta1 + ta2),

813 _1_0 + ta1 * ta2)

814 ta1, b1 = ta2, b2

815

816 if not p2.isequalTo(p1, eps=EPS):

817 z, z2 = _bearingTo2(p1, p2, wrap=wrap)

818 if z1 is not None:

819 D += _w180(z - z1) # (z - z1 + 540) ...

820 D += _w180(z2 - z) # (z2 - z + 540) % 360 - 180

821 p1, z1 = p2, z2

822

823 R = abs(R * _2_0)

824 if abs(D) < _90_0 or polar: # ispolar(points)

825 R = abs(R - PI2)

826 if radius:

827 a = degrees(A.fover(len(A))) # mean lat

828 R *= _mean_radius(radius, a)**2

829 return float(R)

830

831

832 def _destination2(a, b, r, t):

833 '''(INTERNAL) Destination lat- and longitude in C{radians}.

834

835 @arg a: Latitude (C{radians}).

836 @arg b: Longitude (C{radians}).

837 @arg r: Angular distance (C{radians}).

838 @arg t: Bearing (compass C{radians}).

839

840 @return: 2-Tuple (phi, lam) of (C{radians}, C{radiansPI}).

841 '''

842 # see <https://www.EdWilliams.org/avform.htm#LL>

843 sa, ca, sr, cr, st, ct = sincos2_(a, r, t)

844 ca *= sr

845

846 a = asin1(ct * ca + cr * sa)

847 d = atan2(st * ca, cr - sa * sin(a))

848 # note, in EdWilliams.org/avform.htm W is + and E is -

849 return a, (b + d) # (mod(b + d + PI, PI2) - PI)

850

851

852 def _int3d2(s, end, wrap, _i_, Vector, hs):

853 # see <https://www.EdWilliams.org/intersect.htm> (5) ff

854 # and similar logic in .ellipsoidalBaseDI._intersect3

855 a1, b1 = s.philam

856

857 if _isDegrees(end): # bearing, get pseudo-end point

858 a2, b2 = _destination2(a1, b1, PI_4, radians(end))

859 else: # must be a point

860 s.others(end, name=_end_ + _i_)

861 hs.append(end.height)

862 a2, b2 = end.philam

863 if wrap:

864 a2, b2 = _Wrap.philam(a2, b2)

865

866 db, b2 = unrollPI(b1, b2, wrap=wrap)

867 if max(fabs(db), fabs(a2 - a1)) < EPS:

868 raise _ValueError(_SPACE_(_line_ + _i_, _null_))

869 # note, in EdWilliams.org/avform.htm W is + and E is -

870 sb21, cb21, sb12, cb12 = sincos2_(db * _0_5,

871 -(b1 + b2) * _0_5)

872 cb21 *= sin(a1 - a2) # sa21

873 sb21 *= sin(a1 + a2) # sa12

874 x = Vector(fdot_(sb12, cb21, -cb12, sb21),

875 fdot_(cb12, cb21, sb12, sb21),

876 cos(a1) * cos(a2) * sin(db)) # ll=start

877 return x.unit(), (db, (a2 - a1)) # negated d

878

879

880 def _intdot(ds, a1, b1, a, b, wrap):

881 # compute dot product ds . (-b + b1, a - a1)

882 db, _ = unrollPI(b1, b, wrap=wrap)

883 return fdot(ds, db, a - a1)

884

885

886 def intersecant2(center, circle, point, other, **radius_exact_height_wrap):

887 '''Compute the intersections of a circle and a (great circle) line given as

888 two points or as a point and a bearing from North.

889

890 @arg center: Center of the circle (L{LatLon}).

891 @arg circle: Radius of the circle (C{meter}, same units as the earth

892 B{C{radius}}) or a point on the circle (L{LatLon}).

893 @arg point: A point on the (great circle) line (L{LatLon}).

894 @arg other: An other point on the (great circle) line (L{LatLon}) or

895 the bearing at the B{C{point}} (compass C{degrees360}).

896 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method

897 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase.

898 intersecant2>} for further details.

899

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

901 an instance of the B{C{point}} class. Both points are the same

902 instance if the (great circle) line is tangent to the circle.

903

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

905

906 @raise TypeError: If B{C{center}}, B{C{point}}, B{C{circle}} or B{C{other}}

907 not L{LatLon}.

908

909 @raise UnitError: Invalid B{C{circle}}, B{C{other}}, B{C{radius}},

910 B{C{exact}}, B{C{height}} or B{C{napieradius}}.

911 '''

912 c = _T00.others(center=center)

913 p = _T00.others(point=point)

914 try:

915 return _intersecant2(c, circle, p, other, **radius_exact_height_wrap)

916 except (TypeError, ValueError) as x:

917 raise _xError(x, center=center, circle=circle, point=point, other=other,

918 **radius_exact_height_wrap)

919

920

921 def _intersect(start1, end1, start2, end2, height=None, wrap=False, # in.ellipsoidalBaseDI._intersect3

922 LatLon=LatLon, **LatLon_kwds):

923 # (INTERNAL) Intersect two (spherical) lines, see L{intersection}

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

925

926 s1, s2 = start1, start2

927 if wrap:

928 s2 = _Wrap.point(s2)

929 hs = [s1.height, s2.height]

930

931 a1, b1 = s1.philam

932 a2, b2 = s2.philam

933 db, b2 = unrollPI(b1, b2, wrap=wrap)

934 r12 = vincentys_(a2, a1, db)

935 if fabs(r12) < EPS: # [nearly] coincident points

936 a, b = favg(a1, a2), favg(b1, b2)

937

938 # see <https://www.EdWilliams.org/avform.htm#Intersection>

939 elif _isDegrees(end1) and _isDegrees(end2): # both bearings

940 sa1, ca1, sa2, ca2, sr12, cr12 = sincos2_(a1, a2, r12)

941

942 x1, x2 = (sr12 * ca1), (sr12 * ca2)

943 if isnear0(x1) or isnear0(x2):

944 raise IntersectionError(_parallel_)

945 # handle domain error for equivalent longitudes,

946 # see also functions asin_safe and acos_safe at

947 # <https://www.EdWilliams.org/avform.htm#Math>

948 t12, t13 = acos1((sa2 - sa1 * cr12) / x1), radiansPI2(end1)

949 t21, t23 = acos1((sa1 - sa2 * cr12) / x2), radiansPI2(end2)

950 if sin(db) > 0:

951 t21 = PI2 - t21

952 else:

953 t12 = PI2 - t12

954 sx1, cx1, sx2, cx2 = sincos2_(wrapPI(t13 - t12), # angle 2-1-3

955 wrapPI(t21 - t23)) # angle 1-2-3)

956 if isnear0(sx1) and isnear0(sx2):

957 raise IntersectionError(_infinite_)

958 sx3 = sx1 * sx2

959# XXX if sx3 < 0:

960# XXX raise ValueError(_ambiguous_)

961 x3 = acos1(cr12 * sx3 - cx2 * cx1)

962 r13 = atan2(sr12 * sx3, cx2 + cx1 * cos(x3))

963

964 a, b = _destination2(a1, b1, r13, t13)

965 # like .ellipsoidalBaseDI,_intersect3, if this intersection

966 # is "before" the first point, use the antipodal intersection

967 if opposing_(t13, bearing_(a1, b1, a, b, wrap=wrap)):

968 a, b = antipode_(a, b) # PYCHOK PhiLam2Tuple

969

970 else: # end point(s) or bearing(s)

971 _N = _MODS.nvectorBase._N_Vector

972

973 x1, d1 = _int3d2(s1, end1, wrap, _1_, _N, hs)

974 x2, d2 = _int3d2(s2, end2, wrap, _2_, _N, hs)

975 x = x1.cross(x2)

976 if x.length < EPS: # [nearly] colinear or parallel lines

977 raise IntersectionError(_colinear_)

978 a, b = x.philam

979 # choose intersection similar to sphericalNvector

980 if not (_intdot(d1, a1, b1, a, b, wrap) *

981 _intdot(d2, a2, b2, a, b, wrap)) > 0:

982 a, b = antipode_(a, b) # PYCHOK PhiLam2Tuple

983

984 h = fmean(hs) if height is None else Height(height)

985 return _LL3Tuple(degrees90(a), degrees180(b), h,

986 intersection, LatLon, LatLon_kwds)

987

988

989 def intersection(start1, end1, start2, end2, height=None, wrap=False,

990 **LatLon_and_kwds):

991 '''Compute the intersection point of two lines, each defined by

992 two points or by a start point and a bearing from North.

993

994 @arg start1: Start point of the first line (L{LatLon}).

995 @arg end1: End point of the first line (L{LatLon}) or the bearing

996 at the first start point (compass C{degrees360}).

997 @arg start2: Start point of the second line (L{LatLon}).

998 @arg end2: End point of the second line (L{LatLon}) or the bearing

999 at the second start point (compass C{degrees360}).

1000 @kwarg height: Optional height for the intersection point,

1001 overriding the mean height (C{meter}).

1002 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{start2}}

1003 and both B{C{end*}} points (C{bool}).

1004 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to use

1005 for the intersection point and optionally additional

1006 B{C{LatLon}} keyword arguments, ignored if C{B{LatLon}

1007 is None}.

1008

1009 @return: The intersection point as a (B{C{LatLon}}) or if C{B{LatLon}

1010 is None} a L{LatLon3Tuple}C{(lat, lon, height)}. An alternate

1011 intersection point might be the L{antipode} to the returned result.

1012

1013 @raise IntersectionError: Ambiguous or infinite intersection or colinear,

1014 parallel or otherwise non-intersecting lines.

1015

1016 @raise TypeError: A B{C{start1}}, B{C{end1}}, B{C{start2}} or B{C{end2}}

1017 point not L{LatLon}.

1018

1019 @raise ValueError: Invalid B{C{height}} or C{null} line.

1020 '''

1021 s1 = _T00.others(start1=start1)

1022 s2 = _T00.others(start2=start2)

1023 try:

1024 return _intersect(s1, end1, s2, end2, height=height, wrap=wrap, **LatLon_and_kwds)

1025 except (TypeError, ValueError) as x:

1026 raise _xError(x, start1=start1, end1=end1, start2=start2, end2=end2)

1027

1028

1029 def intersections2(center1, rad1, center2, rad2, radius=R_M, eps=_0_0,

1030 height=None, wrap=False, # was=True

1031 **LatLon_and_kwds):

1032 '''Compute the intersection points of two circles each defined by a

1033 center point and a radius.

1034

1035 @arg center1: Center of the first circle (L{LatLon}).

1036 @arg rad1: Radius of the first circle (C{meter} or C{radians}, see

1037 B{C{radius}}).

1038 @arg center2: Center of the second circle (L{LatLon}).

1039 @arg rad2: Radius of the second circle (C{meter} or C{radians}, see

1040 B{C{radius}}).

1041 @kwarg radius: Mean earth radius (C{meter} or C{None} if B{C{rad1}},

1042 B{C{rad2}} and B{C{eps}} are given in C{radians}).

1043 @kwarg eps: Required overlap (C{meter} or C{radians}, see B{C{radius}}).

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

1045 conventionally) or C{None} for the I{"radical height"}

1046 at the I{radical line} between both centers.

1047 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{center2}}

1048 (C{bool}).

1049 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to use for

1050 the intersection points and optionally additional B{C{LatLon}}

1051 keyword arguments, ignored if C{B{LatLon} is None}.

1052

1053 @return: 2-Tuple of the intersection points, each a B{C{LatLon}}

1054 instance or if C{B{LatLon} is None} a L{LatLon3Tuple}C{(lat,

1055 lon, height)}. For abutting circles, both intersection

1056 points are the same instance, aka the I{radical center}.

1057

1058 @raise IntersectionError: Concentric, antipodal, invalid or

1059 non-intersecting circles.

1060

1061 @raise TypeError: If B{C{center1}} or B{C{center2}} not L{LatLon}.

1062

1063 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}}, B{C{radius}},

1064 B{C{eps}} or B{C{height}}.

1065

1066 @note: Courtesy of U{Samuel Čavoj<https://GitHub.com/mrJean1/PyGeodesy/issues/41>}.

1067

1068 @see: This U{Answer<https://StackOverflow.com/questions/53324667/

1069 find-intersection-coordinates-of-two-circles-on-earth/53331953>}.

1070 '''

1071 c1 = _T00.others(center1=center1)

1072 c2 = _T00.others(center2=center2)

1073 try:

1074 return _intersects2(c1, rad1, c2, rad2, radius=radius, eps=eps,

1075 height=height, wrap=wrap,

1076 **LatLon_and_kwds)

1077 except (TypeError, ValueError) as x:

1078 raise _xError(x, center1=center1, rad1=rad1,

1079 center2=center2, rad2=rad2, wrap=wrap)

1080

1081

1082 def _intersects2(c1, rad1, c2, rad2, radius=R_M, eps=_0_0, # in .ellipsoidalBaseDI._intersects2

1083 height=None, too_d=None, wrap=False, # was=True

1084 LatLon=LatLon, **LatLon_kwds):

1085 # (INTERNAL) Intersect two spherical circles, see L{intersections2}

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

1087

1088 def _dest3(bearing, h):

1089 a, b = _destination2(a1, b1, r1, bearing)

1090 return _LL3Tuple(degrees90(a), degrees180(b), h,

1091 intersections2, LatLon, LatLon_kwds)

1092

1093 a1, b1 = c1.philam

1094 a2, b2 = c2.philam

1095 if wrap:

1096 a2, b2 = _Wrap.philam(a2, b2)

1097

1098 r1, r2, f = _rads3(rad1, rad2, radius)

1099 if f: # swapped radii, swap centers

1100 a1, a2 = a2, a1 # PYCHOK swap!

1101 b1, b2 = b2, b1 # PYCHOK swap!

1102

1103 db, b2 = unrollPI(b1, b2, wrap=wrap)

1104 d = vincentys_(a2, a1, db) # radians

1105 if d < max(r1 - r2, EPS):

1106 raise IntersectionError(_near_(_concentric_)) # XXX ConcentricError?

1107

1108 r = eps if radius is None else (m2radians(

1109 eps, radius=radius) if eps else _0_0)

1110 if r < _0_0:

1111 raise _ValueError(eps=r)

1112

1113 x = fsumf_(r1, r2, -d) # overlap

1114 if x > max(r, EPS):

1115 sd, cd, sr1, cr1, _, cr2 = sincos2_(d, r1, r2)

1116 x = sd * sr1

1117 if isnear0(x):

1118 raise _ValueError(_invalid_)

1119 x = acos1((cr2 - cd * cr1) / x) # 0 <= x <= PI

1120

1121 elif x < r: # PYCHOK no cover

1122 t = (d * radius) if too_d is None else too_d

1123 raise IntersectionError(_too_(_Fmt.distant(t)))

1124

1125 if height is None: # "radical height"

1126 f = _radical2(d, r1, r2).ratio

1127 h = Height(favg(c1.height, c2.height, f=f))

1128 else:

1129 h = Height(height)

1130

1131 b = bearing_(a1, b1, a2, b2, final=False, wrap=wrap)

1132 if x < EPS4: # externally ...

1133 r = _dest3(b, h)

1134 elif x > _PI_EPS4: # internally ...

1135 r = _dest3(b + PI, h)

1136 else:

1137 return _dest3(b + x, h), _dest3(b - x, h)

1138 return r, r # ... abutting circles

1139

1140

1141@deprecated_function

1142 def isPoleEnclosedBy(points, wrap=False): # PYCHOK no cover

1143 '''DEPRECATED, use function L{pygeodesy.ispolar}.

1144 '''

1145 return ispolar(points, wrap=wrap)

1146

1147

1148 def _LL3Tuple(lat, lon, height, where, LatLon, LatLon_kwds):

1149 '''(INTERNAL) Helper for L{intersection}, L{intersections2} and L{meanOf}.

1150 '''

1151 n = typename(where)

1152 if LatLon is None:

1153 r = LatLon3Tuple(lat, lon, height, name=n)

1154 else:

1155 kwds = _xkwds(LatLon_kwds, height=height, name=n)

1156 r = LatLon(lat, lon, **kwds)

1157 return r

1158

1159

1160 def meanOf(points, height=None, wrap=False, LatLon=LatLon, **LatLon_kwds):

1161 '''Compute the I{geographic} mean of several points.

1162

1163 @arg points: Points to be averaged (L{LatLon}[]).

1164 @kwarg height: Optional height at mean point, overriding the mean height

1165 (C{meter}).

1166 @kwarg wrap: If C{True}, wrap or I{normalize} the B{C{points}} (C{bool}).

1167 @kwarg LatLon: Optional class to return the mean point (L{LatLon}) or C{None}.

1168 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,

1169 ignored if C{B{LatLon} is None}.

1170

1171 @return: The geographic mean and height (B{C{LatLon}}) or if C{B{LatLon}

1172 is None}, a L{LatLon3Tuple}C{(lat, lon, height)}.

1173

1174 @raise TypeError: Some B{C{points}} are not L{LatLon}.

1175

1176 @raise ValueError: No B{C{points}} or invalid B{C{height}}.

1177 '''

1178 def _N_vs(ps, w):

1179 Ps = _T00.PointsIter(ps, wrap=w)

1180 for p in Ps.iterate(closed=False):

1181 yield p._N_vector

1182

1183 m = _MODS.nvectorBase

1184 # geographic, vectorial mean

1185 n = m.sumOf(_N_vs(points, wrap), h=height, Vector=m.NvectorBase)

1186 lat, lon, h = n.latlonheight

1187 return _LL3Tuple(lat, lon, h, meanOf, LatLon, LatLon_kwds)

1188

1189

1190@deprecated_function

1191 def nearestOn2(point, points, **closed_radius_LatLon_options): # PYCHOK no cover

1192 '''DEPRECATED, use function L{sphericalTrigonometry.nearestOn3}.

1193

1194 @return: ... 2-tuple C{(closest, distance)} of the C{closest}

1195 point (L{LatLon}) on the polygon and the C{distance}

1196 between the C{closest} and the given B{C{point}}. The

1197 C{closest} is a B{C{LatLon}} or a L{LatLon2Tuple}C{(lat,

1198 lon)} if C{B{LatLon} is None} ...

1199 '''

1200 ll, d, _ = nearestOn3(point, points, **closed_radius_LatLon_options) # PYCHOK 3-tuple

1201 if _xkwds_get(closed_radius_LatLon_options, LatLon=LatLon) is None:

1202 ll = LatLon2Tuple(ll.lat, ll.lon)

1203 return ll, d

1204

1205

1206 def nearestOn3(point, points, closed=False, radius=R_M, wrap=False, adjust=True,

1207 limit=9, **LatLon_and_kwds):

1208 '''Locate the point on a path or polygon closest to a reference point.

1209

1210 Distances are I{approximated} using function L{equirectangular4

1211 <pygeodesy.equirectangular4>}, subject to the supplied B{C{options}}.

1212

1213 @arg point: The reference point (L{LatLon}).

1214 @arg points: The path or polygon points (L{LatLon}[]).

1215 @kwarg closed: Optionally, close the polygon (C{bool}).

1216 @kwarg radius: Mean earth radius (C{meter}).

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

1218 B{C{points}} (C{bool}).

1219 @kwarg adjust: See function L{equirectangular4<pygeodesy.equirectangular4>}

1220 (C{bool}).

1221 @kwarg limit: See function L{equirectangular4<pygeodesy.equirectangular4>}

1222 (C{degrees}), default C{9 degrees} is about C{1,000 Km} (for

1223 (mean spherical earth radius L{R_KM}).

1224 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=L{LatLon}} to return the

1225 closest point and optionally additional C{B{LatLon}} keyword

1226 arguments or specify C{B{LatLon}=None}.

1227

1228 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} with the

1229 C{closest} point as B{C{LatLon}} or L{LatLon3Tuple}C{(lat,

1230 lon, height)} if C{B{LatLon} is None}. The C{distance} is

1231 the L{equirectangular4<pygeodesy.equirectangular4>} distance

1232 between the C{closest} and the given B{C{point}} converted to

1233 C{meter}, same units as B{C{radius}}. The C{angle} from the

1234 given B{C{point}} to the C{closest} is in compass C{degrees360},

1235 like function L{compassAngle<pygeodesy.compassAngle>}. The

1236 C{height} is the (interpolated) height at the C{closest} point.

1237

1238 @raise LimitError: Lat- and/or longitudinal delta exceeds the B{C{limit}},

1239 see function L{equirectangular4<pygeodesy.equirectangular4>}.

1240

1241 @raise PointsError: Insufficient number of B{C{points}}.

1242

1243 @raise TypeError: Some B{C{points}} are not C{LatLon}.

1244

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

1246

1247 @see: Functions L{equirectangular4<pygeodesy.equirectangular4>} and

1248 L{nearestOn5<pygeodesy.nearestOn5>}.

1249 '''

1250 t = _nearestOn5(point, points, closed=closed, wrap=wrap,

1251 adjust=adjust, limit=limit)

1252 d = degrees2m(t.distance, radius=radius)

1253 h = t.height

1254 n = typename(nearestOn3)

1255

1256 LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=LatLon)

1257 r = LatLon3Tuple(t.lat, t.lon, h, name=n) if LL is None else \

1258 LL(t.lat, t.lon, **_xkwds(kwds, height=h, name=n))

1259 return NearestOn3Tuple(r, d, t.angle, name=n)

1260

1261

1262 def perimeterOf(points, closed=False, radius=R_M, wrap=True):

1263 '''Compute the perimeter of a (spherical) polygon or composite

1264 (with great circle arcs joining the points).

1265

1266 @arg points: The polygon points or clips (L{LatLon}[], L{BooleanFHP}

1267 or L{BooleanGH}).

1268 @kwarg closed: Optionally, close the polygon (C{bool}).

1269 @kwarg radius: Mean earth radius (C{meter}) or C{None}.

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

1271 B{C{points}} (C{bool}).

1272

1273 @return: Polygon perimeter (C{meter}, same units as B{C{radius}}

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

1275

1276 @raise PointsError: Insufficient number of B{C{points}}.

1277

1278 @raise TypeError: Some B{C{points}} are not L{LatLon}.

1279

1280 @raise ValueError: Invalid B{C{radius}} or C{B{closed}=False} with

1281 C{B{points}} a composite.

1282

1283 @note: Distances are based on function L{vincentys_<pygeodesy.vincentys_>}.

1284

1285 @see: Functions L{perimeterOf<pygeodesy.perimeterOf>},

1286 L{sphericalNvector.perimeterOf} and L{ellipsoidalKarney.perimeterOf}.

1287 '''

1288 def _rads(ps, c, w): # angular edge lengths in radians

1289 Ps = _T00.PointsIter(ps, loop=1, wrap=w)

1290 a1, b1 = Ps[0].philam

1291 for p in Ps.iterate(closed=c):

1292 a2, b2 = p.philam

1293 db, b2 = unrollPI(b1, b2, wrap=w and not (c and Ps.looped))

1294 yield vincentys_(a2, a1, db)

1295 a1, b1 = a2, b2

1296

1297 if _MODS.booleans.isBoolean(points):

1298 if not closed:

1299 raise _ValueError(closed=closed, points=_composite_)

1300 r = points._sum2(LatLon, perimeterOf, closed=True, radius=radius, wrap=wrap)

1301 else:

1302 r = fsum(_rads(points, closed, wrap))

1303 return _radians2m(r, radius)

1304

1305

1306 def triangle7(latA, lonA, latB, lonB, latC, lonC, radius=R_M,

1307 excess=excessAbc_,

1308 wrap=False):

1309 '''Compute the angles, sides, and area of a (spherical) triangle.

1310

1311 @arg latA: First corner latitude (C{degrees}).

1312 @arg lonA: First corner longitude (C{degrees}).

1313 @arg latB: Second corner latitude (C{degrees}).

1314 @arg lonB: Second corner longitude (C{degrees}).

1315 @arg latC: Third corner latitude (C{degrees}).

1316 @arg lonC: Third corner longitude (C{degrees}).

1317 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},

1318 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})

1319 or C{None}.

1320 @kwarg excess: I{Spherical excess} callable (L{excessAbc_},

1321 L{excessGirard_} or L{excessLHuilier_}).

1322 @kwarg wrap: If C{True}, wrap and L{unroll180<pygeodesy.unroll180>}

1323 longitudes (C{bool}).

1324

1325 @return: A L{Triangle7Tuple}C{(A, a, B, b, C, c, area)} with

1326 spherical angles C{A}, C{B} and C{C}, angular sides

1327 C{a}, C{b} and C{c} all in C{degrees} and C{area}

1328 in I{square} C{meter} or same units as B{C{radius}}

1329 I{squared} or if C{B{radius}=0} or C{None}, a

1330 L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)} with

1331 I{spherical deficit} C{D} and I{spherical excess}

1332 C{E} as the C{unit area}, all in C{radians}.

1333 '''

1334 t = triangle8_(Phid(latA=latA), Lamd(lonA=lonA),

1335 Phid(latB=latB), Lamd(lonB=lonB),

1336 Phid(latC=latC), Lamd(lonC=lonC),

1337 excess=excess, wrap=wrap)

1338 return _t7Tuple(t, radius)

1339

1340

1341 def triangle8_(phiA, lamA, phiB, lamB, phiC, lamC, excess=excessAbc_,

1342 wrap=False):

1343 '''Compute the angles, sides, I{spherical deficit} and I{spherical

1344 excess} of a (spherical) triangle.

1345

1346 @arg phiA: First corner latitude (C{radians}).

1347 @arg lamA: First corner longitude (C{radians}).

1348 @arg phiB: Second corner latitude (C{radians}).

1349 @arg lamB: Second corner longitude (C{radians}).

1350 @arg phiC: Third corner latitude (C{radians}).

1351 @arg lamC: Third corner longitude (C{radians}).

1352 @kwarg excess: I{Spherical excess} callable (L{excessAbc_},

1353 L{excessGirard_} or L{excessLHuilier_}).

1354 @kwarg wrap: If C{True}, L{unrollPI<pygeodesy.unrollPI>} the

1355 longitudinal deltas (C{bool}).

1356

1357 @return: A L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)} with

1358 spherical angles C{A}, C{B} and C{C}, angular sides

1359 C{a}, C{b} and C{c}, I{spherical deficit} C{D} and

1360 I{spherical excess} C{E}, all in C{radians}.

1361 '''

1362 def _a_r(w, phiA, lamA, phiB, lamB, phiC, lamC):

1363 d, _ = unrollPI(lamB, lamC, wrap=w)

1364 a = vincentys_(phiC, phiB, d)

1365 return a, (phiB, lamB, phiC, lamC, phiA, lamA) # rotate A, B, C

1366

1367 def _A_r(a, sa, ca, sb, cb, sc, cc):

1368 s = sb * sc

1369 A = acos1((ca - cb * cc) / s) if isnon0(s) else a

1370 return A, (sb, cb, sc, cc, sa, ca) # rotate sincos2_'s

1371

1372 # notation: side C{a} is oposite to corner C{A}, etc.

1373 a, r = _a_r(wrap, phiA, lamA, phiB, lamB, phiC, lamC)

1374 b, r = _a_r(wrap, *r)

1375 c, _ = _a_r(wrap, *r)

1376

1377 A, r = _A_r(a, *sincos2_(a, b, c))

1378 B, r = _A_r(b, *r)

1379 C, _ = _A_r(c, *r)

1380

1381 D = fsumf_(PI2, -a, -b, -c) # deficit aka defect

1382 E = excessGirard_(A, B, C) if _isin(excess, excessGirard_, True) else (

1383 excessLHuilier_(a, b, c) if _isin(excess, excessLHuilier_, False) else

1384 excessAbc_(*max((A, b, c), (B, c, a), (C, a, b))))

1385

1386 return Triangle8Tuple(A, a, B, b, C, c, D, E)

1387

1388

1389 def _t7Tuple(t, radius):

1390 '''(INTERNAL) Convert a L{Triangle8Tuple} to L{Triangle7Tuple}.

1391 '''

1392 if radius: # not _isin(radius, None, _0_0)

1393 r = radius if _isRadius(radius) else \

1394 _ellipsoidal_datum(radius).ellipsoid.Rmean

1395 A, B, C = map1(degrees, t.A, t.B, t.C)

1396 t = Triangle7Tuple(A, (r * t.a),

1397 B, (r * t.b),

1398 C, (r * t.c), t.E * r**2)

1399 return t

1400

1401

1402 __all__ += _ALL_OTHER(Cartesian, LatLon, # classes

1403 areaOf, # functions

1404 intersecant2, intersection, intersections2, ispolar,

1405 isPoleEnclosedBy, # DEPRECATED, use ispolar

1406 meanOf,

1407 nearestOn2, nearestOn3,

1408 perimeterOf,

1409 sumOf, # XXX == vector3d.sumOf

1410 triangle7, triangle8_)

1411

1412# **) MIT License

1413#

1414# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1415#

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

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

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

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

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

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

1422#

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

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

1425#

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

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

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

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

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

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

1432# OTHER DEALINGS IN THE SOFTWARE.