Coverage for pygeodesy/sphericalTrigonometry.py: 93%

388 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-23 16:31 -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 _ # noqa: E702 ; 

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.latlonBase import _trilaterate5 

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

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

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

41 Triangle7Tuple, Triangle8Tuple 

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

43 Fmt as _Fmt # XXX shadowed 

44from pygeodesy.props import deprecated_function, deprecated_method 

45from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \ 

46 _intersecant2, LatLonSphericalBase, \ 

47 _rads3, _radians2m 

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

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

50 Phid, Radius_, Scalar 

51from 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 

55from pygeodesy.vector3d import sumOf, Vector3d 

56 

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

58 

59__all__ = _ALL_LAZY.sphericalTrigonometry 

60__version__ = '25.05.12' 

61 

62_PI_EPS4 = PI - EPS4 

63if _PI_EPS4 >= PI: 

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

65 

66 

67class 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 

89class 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 

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

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

763 points joined by great circle arcs). 

764 

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

766 or L{BooleanGH}). 

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

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

769 or C{None}. 

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

771 (C{bool}). 

772 

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

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

775 

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

777 

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

779 

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

781 

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

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

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

785 

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

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

788 L{ellipsoidalKarney.areaOf}. 

789 ''' 

790 if _MODS.booleans.isBoolean(points): 

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

792 

793 _at2, _t_2 = atan2, tan_2 

794 _un, _w180 = unrollPI, wrap180 

795 

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

797 p1 = p2 = Ps[0] 

798 a1, b1 = p1.philam 

799 ta1, z1 = _t_2(a1), None 

800 

801 A = Fsum() # mean phi 

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

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

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

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

806 D = Fsum() 

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

808 a2, b2 = p2.philam 

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

810 A += a2 

811 ta2 = _t_2(a2) 

812 tdb = _t_2(db, points=i) 

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

814 _1_0 + ta1 * ta2) 

815 ta1, b1 = ta2, b2 

816 

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

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

819 if z1 is not None: 

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

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

822 p1, z1 = p2, z2 

823 

824 R = abs(R * _2_0) 

825 if abs(D) < _90_0: # ispolar(points) 

826 R = abs(R - PI2) 

827 if radius: 

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

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

830 return float(R) 

831 

832 

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

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

835 

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

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

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

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

840 

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

842 ''' 

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

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

845 ca *= sr 

846 

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

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

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

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

851 

852 

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

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

855 # and similar logic in .ellipsoidalBaseDI._intersect3 

856 a1, b1 = s.philam 

857 

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

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

860 else: # must be a point 

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

862 hs.append(end.height) 

863 a2, b2 = end.philam 

864 if wrap: 

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

866 

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

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

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

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

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

872 -(b1 + b2) * _0_5) 

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

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

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

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

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

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

879 

880 

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

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

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

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

885 

886 

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

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

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

890 

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

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

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

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

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

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

897 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method 

898 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase. 

899 intersecant2>} for further details. 

900 

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

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

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

904 

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

906 

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

908 not L{LatLon}. 

909 

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

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

912 ''' 

913 c = _T00.others(center=center) 

914 p = _T00.others(point=point) 

915 try: 

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

917 except (TypeError, ValueError) as x: 

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

919 **radius_exact_height_wrap) 

920 

921 

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

923 LatLon=LatLon, **LatLon_kwds): 

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

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

926 

927 s1, s2 = start1, start2 

928 if wrap: 

929 s2 = _Wrap.point(s2) 

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

931 

932 a1, b1 = s1.philam 

933 a2, b2 = s2.philam 

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

935 r12 = vincentys_(a2, a1, db) 

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

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

938 

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

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

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

942 

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

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

945 raise IntersectionError(_parallel_) 

946 # handle domain error for equivalent longitudes, 

947 # see also functions asin_safe and acos_safe at 

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

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

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

951 if sin(db) > 0: 

952 t21 = PI2 - t21 

953 else: 

954 t12 = PI2 - t12 

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

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

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

958 raise IntersectionError(_infinite_) 

959 sx3 = sx1 * sx2 

960# XXX if sx3 < 0: 

961# XXX raise ValueError(_ambiguous_) 

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

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

964 

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

966 # like .ellipsoidalBaseDI,_intersect3, if this intersection 

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

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

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

970 

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

972 _N_vector_ = _MODS.nvectorBase._N_vector_ 

973 

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

975 x2, d2 = _int3d2(s2, end2, wrap, _2_, _N_vector_, hs) 

976 x = x1.cross(x2) 

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

978 raise IntersectionError(_colinear_) 

979 a, b = x.philam 

980 # choose intersection similar to sphericalNvector 

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

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

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

984 

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

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

987 intersection, LatLon, LatLon_kwds) 

988 

989 

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

991 **LatLon_and_kwds): 

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

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

994 

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

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

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

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

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

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

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

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

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

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

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

1006 for the intersection point and optionally additional 

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

1008 is None}. 

1009 

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

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

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

1013 

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

1015 parallel or otherwise non-intersecting lines. 

1016 

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

1018 point not L{LatLon}. 

1019 

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

1021 ''' 

1022 s1 = _T00.others(start1=start1) 

1023 s2 = _T00.others(start2=start2) 

1024 try: 

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

1026 except (TypeError, ValueError) as x: 

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

1028 

1029 

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

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

1032 **LatLon_and_kwds): 

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

1034 center point and a radius. 

1035 

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

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

1038 B{C{radius}}). 

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

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

1041 B{C{radius}}). 

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

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

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

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

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

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

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

1049 (C{bool}). 

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

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

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

1053 

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

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

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

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

1058 

1059 @raise IntersectionError: Concentric, antipodal, invalid or 

1060 non-intersecting circles. 

1061 

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

1063 

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

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

1066 

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

1068 

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

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

1071 ''' 

1072 c1 = _T00.others(center1=center1) 

1073 c2 = _T00.others(center2=center2) 

1074 try: 

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

1076 height=height, wrap=wrap, 

1077 **LatLon_and_kwds) 

1078 except (TypeError, ValueError) as x: 

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

1080 center2=center2, rad2=rad2, wrap=wrap) 

1081 

1082 

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

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

1085 LatLon=LatLon, **LatLon_kwds): 

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

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

1088 

1089 def _dest3(bearing, h): 

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

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

1092 intersections2, LatLon, LatLon_kwds) 

1093 

1094 a1, b1 = c1.philam 

1095 a2, b2 = c2.philam 

1096 if wrap: 

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

1098 

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

1100 if f: # swapped radii, swap centers 

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

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

1103 

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

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

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

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

1108 

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

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

1111 if r < _0_0: 

1112 raise _ValueError(eps=r) 

1113 

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

1115 if x > max(r, EPS): 

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

1117 x = sd * sr1 

1118 if isnear0(x): 

1119 raise _ValueError(_invalid_) 

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

1121 

1122 elif x < r: # PYCHOK no cover 

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

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

1125 

1126 if height is None: # "radical height" 

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

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

1129 else: 

1130 h = Height(height) 

1131 

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

1133 if x < EPS4: # externally ... 

1134 r = _dest3(b, h) 

1135 elif x > _PI_EPS4: # internally ... 

1136 r = _dest3(b + PI, h) 

1137 else: 

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

1139 return r, r # ... abutting circles 

1140 

1141 

1142@deprecated_function 

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

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

1145 ''' 

1146 return ispolar(points, wrap=wrap) 

1147 

1148 

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

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

1151 ''' 

1152 n = typename(where) 

1153 if LatLon is None: 

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

1155 else: 

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

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

1158 return r 

1159 

1160 

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

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

1163 

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

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

1166 (C{meter}). 

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

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

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

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

1171 

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

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

1174 

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

1176 

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

1178 ''' 

1179 def _N_vs(ps, w): 

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

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

1182 yield p._N_vector 

1183 

1184 m = _MODS.nvectorBase 

1185 # geographic, vectorial mean 

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

1187 lat, lon, h = n.latlonheight 

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

1189 

1190 

1191@deprecated_function 

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

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

1194 

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

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

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

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

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

1200 ''' 

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

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

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

1204 return ll, d 

1205 

1206 

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

1208 limit=9, **LatLon_and_kwds): 

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

1210 

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

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

1213 

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

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

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

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

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

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

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

1221 (C{bool}). 

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

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

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

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

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

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

1228 

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

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

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

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

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

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

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

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

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

1238 

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

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

1241 

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

1243 

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

1245 

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

1247 

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

1249 L{nearestOn5<pygeodesy.nearestOn5>}. 

1250 ''' 

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

1252 adjust=adjust, limit=limit) 

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

1254 h = t.height 

1255 n = typename(nearestOn3) 

1256 

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

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

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

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

1261 

1262 

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

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

1265 (with great circle arcs joining the points). 

1266 

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

1268 or L{BooleanGH}). 

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

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

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

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

1273 

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

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

1276 

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

1278 

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

1280 

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

1282 C{B{points}} a composite. 

1283 

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

1285 

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

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

1288 ''' 

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

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

1291 a1, b1 = Ps[0].philam 

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

1293 a2, b2 = p.philam 

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

1295 yield vincentys_(a2, a1, db) 

1296 a1, b1 = a2, b2 

1297 

1298 if _MODS.booleans.isBoolean(points): 

1299 if not closed: 

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

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

1302 else: 

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

1304 return _radians2m(r, radius) 

1305 

1306 

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

1308 excess=excessAbc_, 

1309 wrap=False): 

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

1311 

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

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

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

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

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

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

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

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

1320 or C{None}. 

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

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

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

1324 longitudes (C{bool}). 

1325 

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

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

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

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

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

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

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

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

1334 ''' 

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

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

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

1338 excess=excess, wrap=wrap) 

1339 return _t7Tuple(t, radius) 

1340 

1341 

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

1343 wrap=False): 

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

1345 excess} of a (spherical) triangle. 

1346 

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

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

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

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

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

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

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

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

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

1356 longitudinal deltas (C{bool}). 

1357 

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

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

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

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

1362 ''' 

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

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

1365 a = vincentys_(phiC, phiB, d) 

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

1367 

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

1369 s = sb * sc 

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

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

1372 

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

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

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

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

1377 

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

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

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

1381 

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

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

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

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

1386 

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

1388 

1389 

1390def _t7Tuple(t, radius): 

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

1392 ''' 

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

1394 r = radius if _isRadius(radius) else \ 

1395 _ellipsoidal_datum(radius).ellipsoid.Rmean 

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

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

1398 B, (r * t.b), 

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

1400 return t 

1401 

1402 

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

1404 areaOf, # functions 

1405 intersecant2, intersection, intersections2, ispolar, 

1406 isPoleEnclosedBy, # DEPRECATED, use ispolar 

1407 meanOf, 

1408 nearestOn2, nearestOn3, 

1409 perimeterOf, 

1410 sumOf, # XXX == vector3d.sumOf 

1411 triangle7, triangle8_) 

1412 

1413# **) MIT License 

1414# 

1415# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

1416# 

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

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

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

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

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

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

1423# 

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

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

1426# 

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

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

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

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

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

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

1433# OTHER DEALINGS IN THE SOFTWARE.