Coverage for pygeodesy/sphericalTrigonometry.py: 93%

387 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-04 12:01 -0400

1 

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

3 

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

5 

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

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

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

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

10 

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

12spherical trigonometry, transcoded from JavaScript originals by 

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

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

15''' 

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

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

18 

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

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

21 isnear0, isnear1, isnon0, _0_0, _0_5, \ 

22 _1_0, _2_0, _90_0 

23from pygeodesy.datums import _ellipsoidal_datum, _mean_radius 

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

25 _TypeError, _ValueError, IntersectionError, \ 

26 _xError, _xkwds, _xkwds_get, _xkwds_pop2 

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

28from pygeodesy.fsums import Fsum, fsum, fsumf_ 

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

30 excessGirard_, excessLHuilier_, opposing_, _radical2, \ 

31 vincentys_ 

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

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

34 _concentric_, _convex_, _end_, _infinite_, \ 

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

36 _point_, _SPACE_, _too_ 

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

38# from pygeodesy.nvectorBase import NvectorBase, sumOf # _MODE 

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

40 Triangle7Tuple, Triangle8Tuple 

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

42 Fmt as _Fmt # XXX shadowed 

43from pygeodesy.props import deprecated_function, deprecated_method 

44from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \ 

45 _intersecant2, LatLonSphericalBase, \ 

46 _rads3, _radians2m, _trilaterate5 

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

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

49 Phid, Radius_, Scalar 

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

51 degrees180, degrees2m, m2radians, radiansPI2, \ 

52 sincos2_, tan_2, unrollPI, _unrollon, _unrollon3, \ 

53 wrap180, wrapPI, _Wrap 

54from pygeodesy.vector3d import sumOf, Vector3d 

55 

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

57 

58__all__ = _ALL_LAZY.sphericalTrigonometry 

59__version__ = '25.04.14' 

60 

61_PI_EPS4 = PI - EPS4 

62if _PI_EPS4 >= PI: 

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

64 

65 

66class Cartesian(CartesianSphericalBase): 

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

68 spherical, geodetic L{LatLon}. 

69 ''' 

70 

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

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

73 

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

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

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

77 

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

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

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

81 

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

83 ''' 

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

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

86 

87 

88class LatLon(LatLonSphericalBase): 

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

90 ''' 

91 

92 def _ab1_ab2_db5(self, other, wrap): 

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

94 ''' 

95 a1, b1 = self.philam 

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

97 if wrap: 

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

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

100 else: # unrollPI shortcut 

101 db = b2 - b1 

102 return a1, b1, a2, b2, db 

103 

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

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

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

107 end point. 

108 

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

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

111 from the start point to the point where the perpendicular 

112 crosses the line. 

113 

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

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

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

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

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

119 

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

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

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

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

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

125 B{C{start}} point. 

126 

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

128 

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

130 ''' 

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

132 cx = cos(x) 

133 return _0_0 if isnear0(cx) else \ 

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

135 

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

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

138 ''' 

139 s = self.others(start=start) 

140 e = self.others(end=end) 

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

142 

143 r = Radius_(radius) 

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

145 

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

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

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

149 return r, x, -b 

150 

151 @deprecated_method 

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

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

154 ''' 

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

156 

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

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

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

160 

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

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

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

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

165 

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

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

168 ''' 

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

170 sa, ca, sa1, ca1, \ 

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

172 sa1 *= ca2 * ca 

173 

174 x = sa1 * sdb 

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

176 z = ca1 * sdb * ca2 * sa 

177 

178 h = hypot(x, y) 

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

180 return None # great circle doesn't reach latitude 

181 

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

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

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

185 

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

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

188 circle from a start to an end point. 

189 

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

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

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

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

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

195 

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

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

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

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

200 

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

202 

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

204 ''' 

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

206 return _radians2m(x, radius) 

207 

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

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

210 travelled the given distance on a bearing from North. 

211 

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

213 B{C{radius}}). 

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

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

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

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

218 

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

220 

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

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

223 ''' 

224 a, b = self.philam 

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

226 

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

228 h = self._heigHt(height) 

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

230 

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

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

233 

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

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

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

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

238 

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

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

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

242 

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

244 

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

246 ''' 

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

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

249 

250# @Property_RO 

251# def Ecef(self): 

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

253# ''' 

254# return _MODS.ecef.EcefKarney 

255 

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

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

258 from this point on the bearing from North. 

259 

260 Direction of vector is such that initial bearing vector 

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

262 

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

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

265 overriding the default L{Vector3d}. 

266 @kwarg Vector_kwds: Optional, additional keyword argunents 

267 for B{C{Vector}}. 

268 

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

270 

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

272 ''' 

273 a, b = self.philam 

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

275 

276 sa *= st 

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

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

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

280 

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

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

283 to an other point. 

284 

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

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

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

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

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

290 C{sphericalNvector.LatLon.initialBearingTo}. 

291 

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

293 

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

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

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

297 

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

299 ''' 

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

301 # XXX behavior like sphericalNvector.LatLon.initialBearingTo 

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

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

304 

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

306 

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

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

309 and an other point. 

310 

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

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

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

314 @kwarg height: Optional height, overriding the intermediate 

315 height (C{meter}). 

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

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

318 

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

320 

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

322 

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

324 

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

326 ''' 

327 p = self 

328 f = Scalar(fraction=fraction) 

329 if not isnear0(f): 

330 p = p.others(other) 

331 if wrap: 

332 p = _Wrap.point(p) 

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

334 a1, b1 = self.philam 

335 a2, b2 = p.philam 

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

337 r = vincentys_(a2, a1, db) 

338 sr = sin(r) 

339 if isnon0(sr): 

340 sa1, ca1, sa2, ca2, \ 

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

342 

343 t = f * r 

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

345 b = sin( t) # / sr superflous 

346 

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

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

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

350 

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

352 b = atan2d(y, x) 

353 

354 else: # PYCHOK no cover 

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

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

357 

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

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

360 return p 

361 

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

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

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

365 

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

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

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

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

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

371 C{degrees360}). 

372 @kwarg height: Optional height for intersection point, 

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

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

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

376 

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

378 intersection point might be the L{antipode} to 

379 the returned result. 

380 

381 @raise IntersectionError: Ambiguous or infinite intersection 

382 or colinear, parallel or otherwise 

383 non-intersecting lines. 

384 

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

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

387 

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

389 ''' 

390 try: 

391 s2 = self.others(other) 

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

393 LatLon=self.classof) 

394 except (TypeError, ValueError) as x: 

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

396 other=other, end2=end2, wrap=wrap) 

397 

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

399 height=None, wrap=True): 

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

401 by a center point and a radius. 

402 

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

404 see B{C{radius}}). 

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

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

407 see B{C{radius}}). 

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

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

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

411 B{C{radius}}). 

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

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

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

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

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

417 

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

419 instance. For abutting circles, both intersection 

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

421 

422 @raise IntersectionError: Concentric, antipodal, invalid or 

423 non-intersecting circles. 

424 

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

426 

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

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

429 ''' 

430 try: 

431 c2 = self.others(other) 

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

433 height=height, wrap=wrap, 

434 LatLon=self.classof) 

435 except (TypeError, ValueError) as x: 

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

437 other=other, rad2=rad2, wrap=wrap) 

438 

439 @deprecated_method 

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

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

442 return self.isenclosedBy(points) 

443 

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

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

446 

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

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

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

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

451 

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

453 composite, C{False} otherwise. 

454 

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

456 

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

458 

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

460 

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

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

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

464 ''' 

465 if _MODS.booleans.isBoolean(points): 

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

467 

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

469 n0 = self._N_vector 

470 

471 v2 = Ps[0]._N_vector 

472 p1 = Ps[1] 

473 v1 = p1._N_vector 

474 # check whether this point on same side of all 

475 # polygon edges (to the left or right depending 

476 # on the anti-/clockwise polygon direction) 

477 gc1 = v2.cross(v1) 

478 t0 = gc1.angleTo(n0) > PI_2 

479 s0 = None 

480 # get great-circle vector for each edge 

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

482 if wrap and not Ps.looped: 

483 p2 = _unrollon(p1, p2) 

484 p1 = p2 

485 v2 = p2._N_vector 

486 gc = v1.cross(v2) 

487 t = gc.angleTo(n0) > PI_2 

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

489 return False # outside 

490 

491 # check for convex polygon: angle between 

492 # gc vectors, signed by direction of n0 

493 # (otherwise the test above is not reliable) 

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

495 if s != s0: 

496 if s0 is None: 

497 s0 = s 

498 else: 

499 t = _Fmt.SQUARE(points=i) 

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

501 gc1, v1 = gc, v2 

502 

503 return True # inside 

504 

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

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

507 

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

509 @kwarg height: Optional height for midpoint, overriding 

510 the mean height (C{meter}). 

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

512 may be negative or greater than 1.0. 

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

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

515 

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

517 

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

519 

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

521 

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

523 ''' 

524 if fraction is _0_5: 

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

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

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

528 

529 x = ca2 * cdb + ca1 

530 y = ca2 * sdb 

531 

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

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

534 

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

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

537 else: 

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

539 return r 

540 

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

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

543 

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

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

546 

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

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

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

550 @kwarg wrap_adjust_limit: Optional keyword arguments for functions 

551 L{sphericalTrigonometry.nearestOn3} and 

552 L{pygeodesy.equirectangular4}, 

553 

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

555 

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

557 see function L{pygeodesy.equirectangular4}. 

558 

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

560 is not (yet) supported. 

561 

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

563 

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

565 

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

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

568 ''' 

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

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

571 if not w: 

572 self._notImplemented(within=w) 

573 

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

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

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

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

578# return point1 

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

580# if isnear0(d): 

581# return point1 # or point2 

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

583# return point2 

584# f = a / d 

585# if within: 

586# if f > EPS1: 

587# return point2 

588# elif f < EPS: 

589# return point1 

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

591 

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

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

594 **kwds)[0] 

595 

596 @deprecated_method 

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

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

599 

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

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

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

603 units of B{C{radius}}. 

604 ''' 

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

606 return r.closest, r.distance 

607 

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

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

610 

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

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

613 

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

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

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

617 @kwarg wrap_adjust_limit: Optional keyword arguments for function 

618 L{sphericalTrigonometry.nearestOn3} and 

619 L{pygeodesy.equirectangular4}, 

620 

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

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

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

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

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

626 function L{pygeodesy.compassAngle}. 

627 

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

629 see function L{pygeodesy.equirectangular4}. 

630 

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

632 

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

634 

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

636 

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

638 and L{pygeodesy.nearestOn5}. 

639 ''' 

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

641 LatLon=self.classof, **wrap_adjust_limit) 

642 

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

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

645 

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

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

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

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

650 

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

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

653 and C{M} if available. 

654 

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

656 ''' 

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

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

659 

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

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

662 

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

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

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

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

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

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

669 

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

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

672 

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

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

675 ''' 

676 B = self.others(otherB=otherB) 

677 C = self.others(otherC=otherC) 

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

679 

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

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

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

683 

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

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

686 North at both points. 

687 

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

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

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

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

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

693 

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

695 

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

697 ''' 

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

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

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

701 

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

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

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

705 of three corresponding circles. 

706 

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

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

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

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

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

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

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

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

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

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

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

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

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

720 

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

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

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

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

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

726 

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

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

729 

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

731 largest I{radial} overlap found. 

732 

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

734 respectively farthest intersection margin. 

735 

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

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

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

739 

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

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

742 no intersection or all (near-)concentric if 

743 C{B{area}=False}. 

744 

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

746 

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

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

749 or B{C{radius}}. 

750 ''' 

751 return _trilaterate5(self, distance1, 

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

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

754 area=area, radius=radius, eps=eps, wrap=wrap) 

755 

756 

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

758 

759 

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

761 '''Calculate the area of a (spherical) polygon or composite (with the 

762 points joined by great circle arcs). 

763 

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

765 or L{BooleanGH}). 

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

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

768 or C{None}. 

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

770 (C{bool}). 

771 

772 @return: Polygon area (C{meter} I{quared}, same units as B{C{radius}} 

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

774 

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

776 

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

778 

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

780 

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

782 polygon'<https://MathOverflow.net/questions/97711/ 

783 the-area-of-spherical-polygons>}, 3rd Answer. 

784 

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

786 L{pygeodesy.excessKarney}, L{ellipsoidalExact.areaOf} and 

787 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: # 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 

832def _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 

852def _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 

880def _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 

886def 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 

921def _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_vector_ = _MODS.nvectorBase._N_vector_ 

972 

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

974 x2, d2 = _int3d2(s2, end2, wrap, _2_, _N_vector_, 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 

989def 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 

1029def 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 

1082def _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 

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

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

1144 ''' 

1145 return ispolar(points, wrap=wrap) 

1146 

1147 

1148def _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 

1160def 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 

1191def 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 

1206def 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 

1262def 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 

1306def 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 

1341def 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 

1389def _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-2025 -- 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.