Coverage for pygeodesy/ellipsoidalBaseDI.py: 91%

330 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -0400

1 

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

3 

4u'''(INTERNAL) Private, ellipsoidal Direct/Inverse geodesy base 

5class C{LatLonEllipsoidalBaseDI} and functions. 

6''' 

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

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

9 

10# from pygeodesy.azimuthal import _Equidistants # _MODS 

11from pygeodesy.basics import isLatLon, _xsubclassof, typename 

12from pygeodesy.constants import EPS, MAX, PI, PI2, PI_4, isnear0, isnear1, \ 

13 _EPSqrt as _TOL, _0_0, _0_01, _1_0, _1_5, _3_0 

14# from pygeodesy.dms import F_DMS # _MODS 

15from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase, _TOL_M, property_RO 

16from pygeodesy.errors import _AssertionError, IntersectionError, _IsnotError, _or, \ 

17 _ValueError, _xellipsoidal, _xError, _xkwds_not 

18from pygeodesy.fmath import favg, fmean_ 

19from pygeodesy.fsums import Fmt, fsumf_ 

20from pygeodesy.formy import _isequalTo, opposing, _radical2 

21# from pygeodesy.geodesicw import _Intersecant2, _PlumbTo # _MODS 

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

23from pygeodesy.interns import _antipodal_, _concentric_, _ellipsoidal_, _low_, \ 

24 _exceed_PI_radians_, _near_, _SPACE_, _too_ 

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

26from pygeodesy.namedTuples import Bearing2Tuple, Destination2Tuple, Intersection3Tuple, \ 

27 NearestOn2Tuple, NearestOn8Tuple, _LL4Tuple 

28# from pygeodesy.props import property_RO # from .ellipsoidalBase 

29# from pygeodesy.sphericalNvector import _intersects2 # _MODS 

30# from pygeodesy.sphericalTrigonometry import _intersect, LatLon # _MODS 

31# from pygeodesy.streprs import Fmt # from .fsums 

32from pygeodesy.units import _fi_j2, _isDegrees, _isHeight, _isRadius, Radius_, Scalar 

33from pygeodesy.utily import m2km, unroll180, _unrollon, _unrollon3, _Wrap, wrap360 

34# from pygeodesy.vector3d import _intersects2, _intersect3d3, _nearestOn2, Vector3d # _MODS 

35 

36from math import degrees, radians 

37 

38__all__ = _ALL_LAZY.ellipsoidalBaseDI 

39__version__ = '25.04.21' 

40 

41_polar__ = 'polar?' 

42_TRIPS = 33 # _intersect3, _intersects2, _nearestOn interations, 6..9 sufficient? 

43 

44 

45class LatLonEllipsoidalBaseDI(LatLonEllipsoidalBase): 

46 '''(INTERNAL) Base class for C{ellipsoidal*.LatLon} classes 

47 with I{overloaded} C{Direct} and C{Inverse} methods. 

48 ''' 

49 

50 def bearingTo2(self, other, wrap=False): 

51 '''Compute the initial and final bearing (forward and reverse 

52 azimuth) from this to an other point, using this C{Inverse} 

53 method. See methods L{initialBearingTo} and L{finalBearingTo} 

54 for more details. 

55 

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

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

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

59 

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

61 

62 @raise TypeError: If B{C{other}} not this C{LatLon} class. 

63 

64 @raise ValueError: If this and the B{C{other}} point's L{Datum} 

65 ellipsoids are not compatible. 

66 ''' 

67 r = self._Inverse(other, wrap) 

68 return Bearing2Tuple(r.initial, r.final, name=self.name) 

69 

70 def destination(self, distance, bearing, height=None): 

71 '''Compute the destination point after having travelled for 

72 the given distance from this point along a geodesic given 

73 by an initial bearing. See method L{destination2} for 

74 further details. 

75 

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

77 ''' 

78 return self._Direct(distance, bearing, self.classof, height).destination 

79 

80 def destination2(self, distance, bearing, height=None): 

81 '''Compute the destination point and the final bearing (reverse 

82 azimuth) after having travelled for the given distance from 

83 this point along a geodesic (line) given by an initial bearing 

84 at this point. 

85 

86 The distance must be in the same units as this point's datum's 

87 ellipsoid's axes, conventionally C{meter}. The distance is 

88 measured on the surface of the ellipsoid, ignoring this point's 

89 height. 

90 

91 The initial and final bearing (forward and reverse azimuth) 

92 are in compass C{degrees360}, clockwise from North. 

93 

94 The destination point's height and datum are set to this 

95 point's height and datum, unless the former is overridden. 

96 

97 @arg distance: Distance (C{meter}). 

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

99 @kwarg height: Optional height, overriding the default 

100 height (C{meter}, same units as C{distance}). 

101 

102 @return: A L{Destination2Tuple}C{(destination, final)}. 

103 ''' 

104 return self._Direct(distance, bearing, self.classof, height) 

105 

106 def _Direct(self, distance, bearing, LL, height): # overloaded by I{Vincenty} 

107 '''(INTERNAL) I{Karney}'s C{Direct} method. 

108 

109 @return: A L{Destination2Tuple}C{(destination, final)} or a 

110 L{Destination3Tuple}C{(lat, lon, final)} if C{B{LL} is None}. 

111 ''' 

112 g = self.geodesic 

113 r = g.Direct3(self.lat, self.lon, bearing, distance) 

114 if LL: 

115 r = self._Direct2Tuple(LL, height, r) 

116 return r 

117 

118 def _Direct2Tuple(self, LL, height, r): 

119 '''(INTERNAL) Helper for C{._Direct} result L{Destination2Tuple}. 

120 ''' 

121 h = self._heigHt(height) 

122 d = _xkwds_not(None, datum=self.datum, name=self.name, 

123 epoch=self.epoch, reframe=self.reframe) 

124 d = LL(*_Wrap.latlon(r.lat, r.lon), height=h, **d) 

125 return Destination2Tuple(d, wrap360(r.final), name=self.name) 

126 

127 def distanceTo(self, other, wrap=False, **unused): # radius=R_M 

128 '''Compute the distance between this and an other point along 

129 a geodesic. See method L{distanceTo3} for more details. 

130 

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

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

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

134 

135 @return: Distance (C{meter}). 

136 

137 @raise TypeError: If B{C{other}} not this C{LatLon} class. 

138 

139 @raise ValueError: This and the B{C{other}} point's L{Datum} 

140 ellipsoids are incompatible. 

141 ''' 

142 return self._Inverse(other, wrap, azis=False).distance 

143 

144 def distanceTo3(self, other, wrap=False): 

145 '''Compute the distance, the initial and final bearing along 

146 a geodesic between this and an other point, using this 

147 C{Inverse} method. 

148 

149 The distance is in the same units as this point's datum's 

150 ellipsoid's axes, conventionally meter. The distance is 

151 measured on the surface of the ellipsoid, ignoring this 

152 point's height. 

153 

154 The initial and final bearing (forward and reverse azimuth) 

155 are in compass C{degrees360}, clockwise from North. 

156 

157 @arg other: Destination point (C{LatLon}). 

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

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

160 

161 @return: A L{Distance3Tuple}C{(distance, initial, final)}. 

162 

163 @raise TypeError: If B{C{other}} not this C{LatLon} class. 

164 

165 @raise ValueError: This and the B{C{other}} point's L{Datum} 

166 ellipsoids are not compatible. 

167 ''' 

168 return self._xnamed(self._Inverse(other, wrap)) 

169 

170 def finalBearingOn(self, distance, bearing): 

171 '''Compute the final bearing (reverse azimuth) after having 

172 travelled for the given distance along a geodesic given 

173 by an initial bearing from this point. See method 

174 L{destination2} for more details. 

175 

176 @arg distance: Distance (C{meter}). 

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

178 

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

180 ''' 

181 return self._Direct(distance, bearing, None, None).final 

182 

183 def finalBearingTo(self, other, wrap=False): 

184 '''Compute the final bearing (reverse azimuth) after having 

185 travelled along a geodesic from this point to an other 

186 point. See method L{distanceTo3} for more details. 

187 

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

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

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

191 

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

193 

194 @raise TypeError: If B{C{other}} not this C{LatLon} class. 

195 

196 @raise ValueError: This and the B{C{other}} point's L{Datum} 

197 ellipsoids are incompatible. 

198 ''' 

199 return self._Inverse(other, wrap).final 

200 

201 @property_RO 

202 def geodesic(self): # overloaded by I{Karney}'s, N/A for I{Vincenty} 

203 '''N/A, invalid (C{None} I{always}). 

204 ''' 

205 return None # PYCHOK no cover 

206 

207 def _g_gl_p3(self, start, end, exact, wrap): 

208 '''(INTERNAL) Helper for methods C{.intersecant2} and C{.plumbTo}. 

209 ''' 

210 p = _unrollon(self, self.others(start=start), wrap=wrap) 

211 g = self.datum.ellipsoid.geodesic_(exact=exact) 

212 gl = g._DirectLine( p, end) if _isDegrees(end) else \ 

213 g._InverseLine(p, self.others(end=end), wrap) 

214 return g, gl, p 

215 

216 def initialBearingTo(self, other, wrap=False): 

217 '''Compute the initial bearing (forward azimuth) to travel 

218 along a geodesic from this point to an other point. See 

219 method L{distanceTo3} for more details. 

220 

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

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

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

224 

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

226 

227 @raise TypeError: If B{C{other}} not this C{LatLon} class. 

228 

229 @raise ValueError: If this and the B{C{other}} point's L{Datum} 

230 ellipsoids are incompatible. 

231 ''' 

232 return self._Inverse(other, wrap).initial 

233 

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

235 '''Return the point at given fraction along the geodesic between 

236 this and an other point. 

237 

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

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

240 at this and 1.0 at the other point. 

241 @kwarg height: Optional height, overriding the fractional 

242 height (C{meter}). 

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

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

245 

246 @return: Intermediate point (C{LatLon}). 

247 

248 @raise TypeError: If B{C{other}} not this C{LatLon} class. 

249 

250 @raise UnitError: Invalid B{C{fraction}} or B{C{height}}. 

251 

252 @raise ValueError: This and the B{C{other}} point's L{Datum} 

253 ellipsoids are incompatible. 

254 

255 @see: Methods L{distanceTo3}, L{destination}, C{midpointTo} and 

256 C{rhumbMidpointTo}. 

257 ''' 

258 f = Scalar(fraction=fraction) 

259 if isnear0(f): 

260 r = self 

261 elif isnear1(f) and not wrap: 

262 r = self.others(other) 

263 else: # negative fraction OK 

264 t = self.distanceTo3(other, wrap=wrap) 

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

266 r = self.destination(t.distance * f, t.initial, height=h) 

267 return r 

268 

269 def intersecant2(self, circle, start, end, exact=False, height=None, # PYCHOK signature 

270 wrap=False, tol=_TOL): 

271 '''Compute the intersections of a circle and a geodesic (line) given as two 

272 points or as a point and a bearing from North. 

273 

274 @arg circle: Radius of the circle centered at this location (C{meter}, 

275 conventionally) or a point on the circle (this C{LatLon}). 

276 @arg start: Start point of the geodesic (line) (this C{LatLon}). 

277 @arg end: End point of the geodesic (line) (this C{LatLon}) or the initial 

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

279 @kwarg exact: Exact C{geodesic...} to use (C{bool} or C{Geodesic...}), see 

280 method L{geodesic_<Ellipsoid.geodesic_>}. 

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

282 conventionally) or C{None} for interpolated heights. 

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

284 B{C{start}} and/or B{C{end}} (C{bool}). 

285 @kwarg tol: Convergence tolerance (C{scalar}). 

286 

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

288 each an instance of this C{LatLon} class. Both points are the same 

289 instance if the geodesic (line) is tangential to the circle. 

290 

291 @raise IntersectionError: The circle and geodesic do not intersect. 

292 

293 @raise TypeError: Invalid B{C{circle}}, B{C{start}} or B{C{end}}. 

294 

295 @raise UnitError: Invalid B{C{circle}}, B{C{end}}, B{C{exact}} or B{C{height}}. 

296 

297 @see: Method L{rhumbIntersecant2<LatLonBase.rhumbIntersecant2>}. 

298 ''' 

299 try: 

300 g, gl, p = self._g_gl_p3(start, end, exact, wrap) 

301 r = Radius_(circle=circle) if _isRadius(circle) else \ 

302 g._Inverse(self, self.others(circle=circle), wrap).s12 

303 

304 P, Q = _MODS.geodesicw._Intersecant2(gl, self.lat, self.lon, r, tol=tol, 

305 form=_MODS.dms.F_DMS) 

306 return self._intersecend2(p, end, wrap, height, g, P, Q, 

307 self.intersecant2) 

308 except (TypeError, ValueError) as x: 

309 raise _xError(x, center=self, circle=circle, start=start, end=end, 

310 exact=exact, wrap=wrap) 

311 

312 def _Inverse(self, other, wrap, **unused): # azis=False, overloaded by I{Vincenty} 

313 '''(INTERNAL) I{Karney}'s C{Inverse} method. 

314 

315 @return: A L{Distance3Tuple}C{(distance, initial, final)}. 

316 ''' 

317 _ = self.ellipsoids(other) 

318 g = self.geodesic 

319 _, lon = unroll180(self.lon, other.lon, wrap=wrap) 

320 return g.Inverse3(self.lat, self.lon, other.lat, lon) 

321 

322 def nearestOn8(self, points, closed=False, height=None, wrap=False, 

323 equidistant=None, tol=_TOL_M): 

324 '''I{Iteratively} locate the point on a path or polygon closest 

325 to this point. 

326 

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

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

329 @kwarg height: Optional height, overriding the height of this and all 

330 other points (C{meter}, conventionally). If C{B{height} 

331 is None}, each point's height is taken into account to 

332 compute distances. 

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

334 (C{bool}). 

335 @kwarg equidistant: An azimuthal equidistant projection (I{class} or function 

336 L{pygeodesy.equidistant}) or C{None} for the preferred 

337 L{Equidistant<pygeodesy.ellipsoidalBase.Equidistant>}. 

338 @kwarg tol: Convergence tolerance (C{meter}, conventionally). 

339 

340 @return: A L{NearestOn8Tuple}C{(closest, distance, fi, j, start, end, 

341 initial, final)} with C{distance} in C{meter}, conventionally 

342 and with the C{closest}, the C{start} the C{end} point each 

343 an instance of this C{LatLon} class. 

344 

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

346 

347 @raise TypeError: Some B{C{points}} or B{C{equidistant}} invalid. 

348 

349 @raise ValueError: Some B{C{points}}' datum or ellipsoid incompatible 

350 or no convergence for the given B{C{tol}}. 

351 

352 @see: Function L{pygeodesy.nearestOn6} and method C{nearestOn6}. 

353 ''' 

354 _d3 = self.distanceTo3 # Distance3Tuple 

355 _n3 = _nearestOn3 

356 try: 

357 Ps = self.PointsIter(points, loop=1, wrap=wrap) 

358 p1 = c = s = e = Ps[0] 

359 _ = self.ellipsoids(p1) 

360 c3 = _d3(c, wrap=wrap) # XXX wrap=False? 

361 

362 except (TypeError, ValueError) as x: 

363 raise _xError(x, Fmt.INDEX(points=0), p1, this=self, tol=tol, 

364 closed=closed, height=height, wrap=wrap) 

365 

366 # get the azimuthal equidistant projection, once 

367 A = _Equidistant00(equidistant, c) 

368 b = _Box(c, c3.distance) 

369 m = f = i = 0 # p1..p2 == points[i]..[j] 

370 

371 kwds = dict(within=True, height=height, tol=tol, 

372 LatLon=self.classof, # this LatLon 

373 datum=self.datum, epoch=self.epoch, reframe=self.reframe) 

374 try: 

375 for j, p2 in Ps.enumerate(closed=closed): 

376 if wrap and j != 0: # not Ps.looped 

377 p2 = _unrollon(p1, p2) 

378 # skip edge if no overlap with box around closest 

379 if j < 4 or b.overlaps(p1.lat, p1.lon, p2.lat, p2.lon): 

380 p, t, _ = _n3(self, p1, p2, A, **kwds) 

381 d3 = _d3(p, wrap=False) # already unrolled 

382 if d3.distance < c3.distance: 

383 c3, c, s, e, f = d3, p, p1, p2, (i + t) 

384 b = _Box(c, c3.distance) 

385 m = max(m, c.iteration) 

386 p1, i = p2, j 

387 

388 except (TypeError, ValueError) as x: 

389 raise _xError(x, Fmt.INDEX(points=i), p1, 

390 Fmt.INDEX(points=j), p2, this=self, tol=tol, 

391 closed=closed, height=height, wrap=wrap) 

392 

393 f, j = _fi_j2(f, len(Ps)) # like .vector3d.nearestOn6 

394 

395 n = typename(self) 

396 c.rename(n) 

397 if s is not c: 

398 s = s.copy(name=n) 

399 if e is not c: 

400 e = e.copy(name=n) # name__=self.nearestOn8 

401 return NearestOn8Tuple(c, c3.distance, f, j, s, e, c3.initial, c3.final, 

402 iteration=m) # ._iteration for tests 

403 

404 def plumbTo(self, start, end, exact=False, height=None, # PYCHOK signature 

405 wrap=False, tol=_TOL): 

406 '''Compute the intersection of a geodesic from this point I{perpendicular} to 

407 a geodesic (line) given as two points or as a point and a bearing from North. 

408 

409 @arg start: Start point of the geodesic (line) (this C{LatLon}). 

410 @arg end: End point of the geodesic (line) (this C{LatLon}) or the initial 

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

412 @kwarg exact: Exact C{geodesic...} to use (C{bool} or C{Geodesic...}), 

413 see method L{Ellipsoid.geodesic_}. 

414 @kwarg height: Optional height for the intersection point (C{meter}, 

415 conventionally) or C{None} for an interpolated height. 

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

417 and/or B{C{end}} point (C{bool}). 

418 @kwarg tol: Convergence tolerance (C{meter}). 

419 

420 @return: The intersection point, an instance of this C{LatLon} class. 

421 

422 @raise TypeError: If B{C{start}} or B{C{end}} not this C{LatLon} class. 

423 

424 @raise UnitError: Invalid B{C{end}}, B{C{exact}} or B{C{height}}. 

425 ''' 

426 try: 

427 g, gl, p = self._g_gl_p3(start, end, exact, wrap) 

428 

429 P = _MODS.geodesicw._PlumbTo(gl, self.lat, self.lon, tol=tol) 

430 h = self._havg(p, h=height) 

431 p = self.classof(P.lat2, P.lon2, datum=self.datum, height=h) # name=n 

432 p._iteration = P.iteration 

433 except (TypeError, ValueError) as x: 

434 raise _xError(x, plumb=self, start=start, end=end, 

435 exact=exact, wrap=wrap) 

436 return p 

437 

438 

439class _Box(object): 

440 '''Bounding box around a C{LatLon} point. 

441 

442 @see: Function C{_box4} in .clipy.py. 

443 ''' 

444 _1_01 = _1_0 + _0_01 # ~1% margin 

445 

446 def __init__(self, center, distance): 

447 '''New L{_Box} around point. 

448 

449 @arg center: The center point (C{LatLon}). 

450 @arg distance: Radius, half-size of the box 

451 (C{meter}, conventionally) 

452 ''' 

453 m = Radius_(distance=distance) 

454 E = center.ellipsoid() 

455 d = E.m2degrees(m) * self._1_01 

456 self._N = center.lat + d 

457 self._S = center.lat - d 

458 self._E = center.lon + d 

459 self._W = center.lon - d 

460 

461 def overlaps(self, lat1, lon1, lat2, lon2): 

462 '''Check whether this box overlaps an other box. 

463 

464 @arg lat1: Latitude of a box corner (C{degrees}). 

465 @arg lon1: Longitude of a box corner (C{degrees}). 

466 @arg lat2: Latitude of the opposing corner (C{degrees}). 

467 @arg lon2: Longitude of the opposing corner (C{degrees}). 

468 

469 @return: C{True} if there is some overlap, C{False} 

470 otherwise (C{bool}). 

471 ''' 

472 if lat1 > lat2: 

473 lat1, lat2 = lat2, lat1 

474 if lat1 > self._N or lat2 < self._S: 

475 return False 

476 if lon1 > lon2: 

477 lon1, lon2 = lon2, lon1 

478 if lon1 > self._E or lon2 < self._W: 

479 return False 

480 return True 

481 

482 

483class _Tol(object): 

484 '''Handle a tolerance in C{meter} as C{degrees} and C{meter}. 

485 ''' 

486 _deg = 0 # tol in degrees 

487 _lat = 0 

488 _m = 0 # tol in meter 

489 _min = MAX # degrees 

490 _prev = None 

491 _r = 0 

492 

493 def __init__(self, tol_m, E, lat, *lats): 

494 '''New L{_Tol}. 

495 

496 @arg tol_m: Tolerance (C{meter}, only). 

497 @arg E: Earth ellipsoid (L{Ellipsoid}). 

498 @arg lat: Latitude (C{degrees}). 

499 @arg lats: Additional latitudes (C{degrees}). 

500 ''' 

501 self._lat = fmean_(lat, *lats) if lats else lat 

502 self._r = max(EPS, E.rocMean(self._lat)) 

503 self._m = max(EPS, tol_m) 

504 self._deg = max(EPS, degrees(self._m / self._r)) # NOT E.m2degrees! 

505 

506 @property_RO 

507 def degrees(self): 

508 '''Get this tolerance in C{degrees}. 

509 ''' 

510 return self._deg 

511 

512 def degrees2m(self, deg): 

513 '''Convert B{C{deg}} to meter at the same C{lat} and earth radius. 

514 ''' 

515 return self.radius * radians(deg) / PI2 # NOT E.degrees2m! 

516 

517 def degError(self, Error=_ValueError): 

518 '''Compose an error with the C{deg}rees minimum. 

519 ''' 

520 return self.mError(self.degrees2m(self._min), Error=Error) 

521 

522 def done(self, deg): 

523 '''Check C{deg} vs tolerance and previous value. 

524 ''' 

525 if deg < self._deg or deg == self._prev: 

526 return True 

527 self._min = min(self._min, deg) 

528 self._prev = deg 

529 return False 

530 

531 @property_RO 

532 def lat(self): 

533 '''Get the mean latitude in C{degrees}. 

534 ''' 

535 return self._lat 

536 

537 def mError(self, m, Error=_ValueError): 

538 '''Compose an error with B{C{m}}eter minimum. 

539 ''' 

540 t = _SPACE_(Fmt.tolerance(self.meter), _too_(_low_)) 

541 if m2km(m) > self.meter: 

542 t = _or(t, _antipodal_, _near_(_polar__)) 

543 return Error(Fmt.no_convergence(m), txt=t) 

544 

545 @property_RO 

546 def meter(self): 

547 '''Get this tolerance in C{meter}. 

548 ''' 

549 return self._m 

550 

551 @property_RO 

552 def radius(self): 

553 '''Get the earth radius in C{meter}. 

554 ''' 

555 return self._r 

556 

557 def reset(self): 

558 '''Reset tolerances. 

559 ''' 

560 self._min = MAX # delattrof() 

561 self._prev = None # delattrof() 

562 

563 

564def _Equidistant00(equidistant, p1): 

565 '''(INTERNAL) Get an C{Equidistant*(0, 0, ...)} instance. 

566 ''' 

567 if equidistant is None or not callable(equidistant): 

568 equidistant = p1.Equidistant 

569 else: 

570 _xsubclassof(*_MODS.azimuthal._Equidistants, 

571 equidistant=equidistant) 

572 return equidistant(0, 0, p1.datum) 

573 

574 

575def intersecant2(center, circle, point, other, **exact_height_wrap_tol): 

576 '''Compute the intersections of a circle and a geodesic given as two points 

577 or as a point and (forward) bearing. 

578 

579 @arg center: Center of the circle (C{LatLon}). 

580 @arg circle: Radius of the circle (C{meter}, conventionally) or a point on 

581 the circle (C{LatLon}, as B{C{center}}). 

582 @arg point: A point of the geodesic (C{LatLon}, as B{C{center}}). 

583 @arg other: An other point of the geodesic (C{LatLon}, as B{C{center}}) or 

584 the (forward) bearing at the B{C{point}} (compass C{degrees}). 

585 @kwarg exact_height_wrap_tol: Optional keyword arguments C{B{exact}=False}, 

586 C{B{height}=None}, C{B{wrap}=False} and C{B{tol}}, see method 

587 L{intersecant2<LatLonEllipsoidalBaseDI.intersecant2>}. 

588 

589 @raise NotImplementedError: Method C{intersecant2} not available. 

590 

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

592 points not ellipsoidal or not compatible with B{C{center}}. 

593 

594 @see: Method C{LatLon.intersecant2} of class L{ellipsoidalExact.LatLon}, 

595 L{ellipsoidalKarney.LatLon} or L{ellipsoidalVincenty.LatLon}. 

596 ''' 

597 if not isLatLon(center, ellipsoidal=True): # isinstance(center, LatLonEllipsoidalBase) 

598 raise _IsnotError(_ellipsoidal_, center=center) 

599 return center.intersecant2(circle, point, other, **exact_height_wrap_tol) 

600 

601 

602def _intersect3(s1, end1, s2, end2, height=None, wrap=False, # MCCABE 16 was=True 

603 equidistant=None, tol=_TOL_M, LatLon=None, **LatLon_kwds): 

604 '''(INTERNAL) Intersect two (ellipsoidal) lines, see ellipsoidal method 

605 L{intersection3}, separated to allow callers to embellish any exceptions. 

606 ''' 

607 _LLS = _MODS.sphericalTrigonometry.LatLon 

608 _si = _MODS.sphericalTrigonometry._intersect 

609 _vi3 = _MODS.vector3d._intersect3d3 

610 

611 def _b_d(s, e, w, t, h=_0_0): 

612 # compute opposing and distance 

613 t = s.classof(t.lat, t.lon, height=h, name=t.name) 

614 t = s.distanceTo3(t, wrap=w) # Distance3Tuple 

615 b = opposing(e, t.initial) # "before" start 

616 return b, t.distance 

617 

618 def _b_e(s, e, w, t): 

619 # compute an end point along the initial bearing about 

620 # 1.5 times the distance to the gu-/estimate, at least 

621 # 1/8 and at most 3/8 of the earth perimeter like the 

622 # radians in .sphericalTrigonometry._int3d2 and bearing 

623 # comparison in .sphericaltrigonometry._intb 

624 b, d = _b_d(s, e, w, t, h=t.height) 

625 m = s.ellipsoid().R2x * PI_4 # authalic exact 

626 d = min(max(d * _1_5, m), m * _3_0) 

627 e = s.destination(d, e) 

628 return b, (_unrollon(s, e) if w else e) 

629 

630 def _e_ll(s, e, w, **end): 

631 # return 2-tuple (end, False if bearing else True) 

632 ll = not _isDegrees(e) 

633 if ll: 

634 e = s.others(**end) 

635 if w: # unroll180 == .karney._unroll2 

636 e = _unrollon(s, e) 

637 return e, ll 

638 

639 def _o(o, b, n, s, t, e): 

640 # determine C{o}utside before, on or after start point 

641 if not o: # intersection may be on start 

642 if _isequalTo(s, t, eps=e.degrees): 

643 return o 

644 return -n if b else n 

645 

646 E = s1.ellipsoids(s2) 

647 

648 e1, ll1 = _e_ll(s1, end1, wrap, end1=end1) 

649 e2, ll2 = _e_ll(s2, end2, wrap, end2=end2) 

650 

651 e = _Tol(tol, E, s1.lat, (e1.lat if ll1 else s1.lat), 

652 s2.lat, (e2.lat if ll2 else s2.lat)) 

653 

654 # get the azimuthal equidistant projection 

655 A = _Equidistant00(equidistant, s1) 

656 

657 # gu-/estimate initial intersection, spherically ... 

658 t = _si(_LLS(s1), (_LLS(e1) if ll1 else e1), 

659 _LLS(s2), (_LLS(e2) if ll2 else e2), 

660 height=height, wrap=False, LatLon=_LLS) # unrolled already 

661 h, n = t.height, t.name 

662 

663 if not ll1: 

664 b1, e1 = _b_e(s1, e1, wrap, t) 

665 if not ll2: 

666 b2, e2 = _b_e(s2, e2, wrap, t) 

667 

668 # ... and iterate as Karney describes, for references 

669 # @see: Function L{ellipsoidalKarney.intersection3}. 

670 for i in range(1, _TRIPS): 

671 A.reset(t.lat, t.lon) # gu-/estimate as origin 

672 # convert start and end points to projection 

673 # space and compute an intersection there 

674 v, o1, o2 = _vi3(*A._forwards(s1, e1, s2, e2), 

675 eps=e.meter, useZ=False) 

676 # convert intersection back to geodetic 

677 t, d = A._reverse2(v) 

678 if e.done(d): # below tol or unchanged? 

679 break 

680 else: 

681 raise e.degError(Error=IntersectionError) 

682 

683 # like .sphericalTrigonometry._intersect, if this intersection 

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

685 if not (ll1 or ll2): # end1 and end2 are an initial bearing 

686 b1, _ = _b_d(s1, end1, wrap, t) 

687 if b1: 

688 t = t.antipodal() 

689 b1 = not b1 

690 b2, _ = _b_d(s2, end2, wrap, t) 

691 

692 r = _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=s1, 

693 iteration=i, name=n) 

694 return Intersection3Tuple(r, (o1 if ll1 else _o(o1, b1, 1, s1, t, e)), 

695 (o2 if ll2 else _o(o2, b2, 2, s2, t, e))) 

696 

697 

698def _intersection3(start1, end1, start2, end2, height=None, wrap=False, # was=True 

699 **equidistant_tol_LatLon_and_kwds): 

700 '''(INTERNAL) Iteratively compute the intersection point of two lines, 

701 each defined by two (ellipsoidal) points or an (ellipsoidal) start 

702 point and an initial bearing from North. 

703 ''' 

704 s1 = _xellipsoidal(start1=start1) 

705 s2 = s1.others(start2=start2) 

706 try: 

707 return _intersect3(s1, end1, s2, end2, height=height, wrap=wrap, 

708 **equidistant_tol_LatLon_and_kwds) 

709 except (TypeError, ValueError) as x: 

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

711 

712 

713def _intersections2(center1, radius1, center2, radius2, height=None, wrap=False, # was=True 

714 **equidistant_tol_LatLon_and_kwds): 

715 '''(INTERNAL) Iteratively compute the intersection points of two circles, 

716 each defined by an (ellipsoidal) center point and a radius. 

717 ''' 

718 c1 = _xellipsoidal(center1=center1) 

719 c2 = c1.others(center2=center2) 

720 try: 

721 return _intersects2(c1, radius1, c2, radius2, height=height, wrap=wrap, 

722 **equidistant_tol_LatLon_and_kwds) 

723 except (TypeError, ValueError) as x: 

724 raise _xError(x, center1=center1, radius1=radius1, 

725 center2=center2, radius2=radius2) 

726 

727 

728def _intersects2(c1, radius1, c2, radius2, height=None, wrap=False, # MCCABE 16 was=True 

729 equidistant=None, tol=_TOL_M, LatLon=None, **LatLon_kwds): 

730 '''(INTERNAL) Intersect two (ellipsoidal) circles, see L{_intersections2} 

731 above, separated to allow callers to embellish any exceptions. 

732 ''' 

733 _LLS = _MODS.sphericalTrigonometry.LatLon 

734 _si2 = _MODS.sphericalTrigonometry._intersects2 

735 _vi2 = _MODS.vector3d._intersects2 

736 

737 def _ll4(t, h, n, c): 

738 return _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=c, 

739 iteration=t.iteration, name=n) 

740 

741 r1 = Radius_(radius1=radius1) 

742 r2 = Radius_(radius2=radius2) 

743 

744 E = c1.ellipsoids(c2) 

745 # get the azimuthal equidistant projection 

746 A = _Equidistant00(equidistant, c1) 

747 

748 if r1 < r2: 

749 c1, c2 = c2, c1 

750 r1, r2 = r2, r1 

751 

752 if r1 > (min(E.b, E.a) * PI): 

753 raise _ValueError(_exceed_PI_radians_) 

754 

755 if wrap: # unroll180 == .karney._unroll2 

756 c2 = _unrollon(c1, c2) 

757 

758 # distance between centers and radii are 

759 # measured along the ellipsoid's surface 

760 m = c1.distanceTo(c2, wrap=False) # meter 

761 if m < max(r1 - r2, EPS): 

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

763 if fsumf_(r1, r2, -m) < 0: 

764 raise IntersectionError(_too_(Fmt.distant(m))) 

765 

766 f = _radical2(m, r1, r2).ratio # "radical fraction" 

767 e = _Tol(tol, E, favg(c1.lat, c2.lat, f=f)) 

768 

769 # gu-/estimate initial intersections, spherically ... 

770 t1, t2 = _si2(_LLS(c1), r1, _LLS(c2), r2, radius=e.radius, 

771 height=height, too_d=m, wrap=False) # unrolled already 

772 h, n = t1.height, t1.name 

773 

774 # ... and iterate as Karney describes, for references 

775 # @see: Function L{ellipsoidalKarney.intersections2}. 

776 ts, ta = [], None 

777 for t in ((t1,) if t1 is t2 else (t1, t2)): 

778 for i in range(1, _TRIPS): 

779 A.reset(t.lat, t.lon) # gu-/estimate as origin 

780 # convert centers to projection space and 

781 # compute the intersections there 

782 t1, t2 = A._forwards(c1, c2) 

783 v1, v2 = _vi2(t1, r1, # XXX * t1.scale?, 

784 t2, r2, # XXX * t2.scale?, 

785 sphere=False, too_d=m) 

786 # convert intersections back to geodetic 

787 if v1 is v2: # abutting 

788 t, d = A._reverse2(v1) 

789 else: # consider the closer intersection 

790 t1, d1 = A._reverse2(v1) 

791 t2, d2 = A._reverse2(v2) 

792 t, d = (t1, d1) if d1 < d2 else (t2, d2) 

793 if e.done(d): # below tol or unchanged? 

794 t._iteration = i # _NamedTuple._iteration 

795 ts.append(t) 

796 if v1 is v2: # abutting 

797 ta = t 

798 break 

799 else: 

800 raise e.degError(Error=IntersectionError) 

801 e.reset() 

802 

803 if ta: # abutting circles 

804 pass # PYCHOK no cover 

805 elif len(ts) == 2: 

806 return (_ll4(ts[0], h, n, c1), 

807 _ll4(ts[1], h, n, c2)) 

808 elif len(ts) == 1: # PYCHOK no cover 

809 ta = ts[0] # assume abutting 

810 else: # PYCHOK no cover 

811 raise _AssertionError(ts=ts) 

812 r = _ll4(ta, h, n, c1) 

813 return r, r 

814 

815 

816def _nearestOn2(p, point1, point2, within=True, height=None, wrap=False, # was=True 

817 equidistant=None, tol=_TOL_M, **LatLon_and_kwds): 

818 '''(INTERNAL) Closest point and fraction, like L{_intersects2} above, 

819 separated to allow callers to embellish any exceptions. 

820 ''' 

821 p1 = p.others(point1=point1) 

822 p2 = p.others(point2=point2) 

823 

824 _ = p.ellipsoids(p1) 

825# E = p.ellipsoids(p2) # done in _nearestOn3 

826 

827 # get the azimuthal equidistant projection 

828 A = _Equidistant00(equidistant, p) 

829 

830 p1, p2, _ = _unrollon3(p, p1, p2, wrap) # XXX don't unroll? 

831 r, f, _ = _nearestOn3(p, p1, p2, A, within=within, height=height, 

832 tol=tol, **LatLon_and_kwds) 

833 return NearestOn2Tuple(r, f) 

834 

835 

836def _nearestOn3(p, p1, p2, A, within=True, height=None, tol=_TOL_M, 

837 LatLon=None, **LatLon_kwds): 

838 # Only in function C{_nearestOn2} and method C{nearestOn8} above 

839 _LLS = _MODS.sphericalNvector.LatLon 

840 _V3d = _MODS.vector3d.Vector3d 

841 _vn2 = _MODS.vector3d._nearestOn2 

842 

843 E = p.ellipsoids(p2) 

844 e = _Tol(tol, E, p.lat, p1.lat, p2.lat) 

845 

846 # gu-/estimate initial nearestOn, spherically ... wrap=False, only! 

847 # using sphericalNvector.LatLon.nearestOn for within=False support 

848 t = _LLS(p).nearestOn(_LLS(p1), _LLS(p2), within=within, 

849 height=height) 

850 n, h = t.name, t.height 

851 if height is None: 

852 h1 = p1.height # use heights as pseudo-Z in projection space 

853 h2 = p2.height # to be included in the closest function 

854 h0 = favg(h1, h2) 

855 else: # ignore heights in distances, Z=0 

856 h0 = h1 = h2 = _0_0 

857 

858 # ... and iterate to find the closest (to the origin with .z 

859 # to interpolate height) as Karney describes, for references 

860 # @see: Function L{ellipsoidalKarney.nearestOn}. 

861 vp, f = _V3d(_0_0, _0_0, h0), None 

862 for i in range(1, _TRIPS): 

863 A.reset(t.lat, t.lon) # gu-/estimate as origin 

864 # convert points to projection space and compute 

865 # the nearest one (and its height) there 

866 s, t = A._forwards(p1, p2) 

867 v, f = _vn2(vp, _V3d(s.x, s.y, h1), 

868 _V3d(t.x, t.y, h2), within=within) 

869 # convert nearest one back to geodetic 

870 t, d = A._reverse2(v) 

871 if e.done(d): # below tol or unchanged? 

872 break 

873 else: 

874 raise e.degError() 

875 

876 if height is None: 

877 h = v.z # nearest 

878 elif _isHeight(height): 

879 h = height 

880 r = _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=p, 

881 iteration=i, name=n) 

882 return r, f, e # fraction or None 

883 

884 

885__all__ += _ALL_DOCS(LatLonEllipsoidalBaseDI, intersecant2) 

886del _1_0, _0_01 

887 

888# **) MIT License 

889# 

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

891# 

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

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

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

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

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

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

898# 

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

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

901# 

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

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

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

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

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

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

908# OTHER DEALINGS IN THE SOFTWARE.