Coverage for pygeodesy/ellipsoidalBaseDI.py: 91%

331 statements  

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

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.05.23' 

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): # in .ellipsoidalVincenty.py 

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

120 ''' 

121 h = self._heigHt(height) 

122 n = self.name 

123 d = _xkwds_not(None, datum=self.datum, height=h, name=n, 

124 epoch=self.epoch, reframe=self.reframe) 

125 d = LL(*_Wrap.latlon(r.lat, r.lon), **d) 

126 return Destination2Tuple(d, wrap360(r.final), name=n) 

127 

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

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

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

131 

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

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

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

135 

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

137 

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

139 

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

141 ellipsoids are incompatible. 

142 ''' 

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

144 

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

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

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

148 C{Inverse} method. 

149 

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

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

152 measured on the surface of the ellipsoid, ignoring this 

153 point's height. 

154 

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

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

157 

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

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

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

161 

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

163 

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

165 

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

167 ellipsoids are not compatible. 

168 ''' 

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

170 

171 def finalBearingOn(self, distance, bearing): 

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

173 travelled for the given distance along a geodesic given 

174 by an initial bearing from this point. See method 

175 L{destination2} for more details. 

176 

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

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

179 

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

181 ''' 

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

183 

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

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

186 travelled along a geodesic from this point to an other 

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

188 

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

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

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

192 

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

194 

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

196 

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

198 ellipsoids are incompatible. 

199 ''' 

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

201 

202 @property_RO 

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

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

205 ''' 

206 return None # PYCHOK no cover 

207 

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

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

210 ''' 

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

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

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

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

215 return g, gl, p 

216 

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

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

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

220 method L{distanceTo3} for more details. 

221 

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

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

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

225 

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

227 

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

229 

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

231 ellipsoids are incompatible. 

232 ''' 

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

234 

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

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

237 this and an other point. 

238 

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

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

241 at this and 1.0 at the other point. 

242 @kwarg height: Optional height, overriding the fractional 

243 height (C{meter}). 

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

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

246 

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

248 

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

250 

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

252 

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

254 ellipsoids are incompatible. 

255 

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

257 C{rhumbMidpointTo}. 

258 ''' 

259 f = Scalar(fraction=fraction) 

260 if isnear0(f): 

261 r = self 

262 elif isnear1(f) and not wrap: 

263 r = self.others(other) 

264 else: # negative fraction OK 

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

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

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

268 return r 

269 

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

271 wrap=False, tol=_TOL): 

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

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

274 

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

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

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

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

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

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

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

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

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

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

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

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

287 

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

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

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

291 

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

293 

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

295 

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

297 

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

299 ''' 

300 try: 

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

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

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

304 

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

306 form=_MODS.dms.F_DMS) 

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

308 self.intersecant2) 

309 except (TypeError, ValueError) as x: 

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

311 exact=exact, wrap=wrap) 

312 

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

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

315 

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

317 ''' 

318 _ = self.ellipsoids(other) 

319 g = self.geodesic 

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

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

322 

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

324 equidistant=None, tol=_TOL_M): 

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

326 to this point. 

327 

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

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

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

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

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

333 compute distances. 

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

335 (C{bool}). 

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

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

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

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

340 

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

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

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

344 an instance of this C{LatLon} class. 

345 

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

347 

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

349 

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

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

352 

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

354 ''' 

355 _d3 = self.distanceTo3 # Distance3Tuple 

356 _n3 = _nearestOn3 

357 try: 

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

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

360 _ = self.ellipsoids(p1) 

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

362 

363 except (TypeError, ValueError) as x: 

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

365 closed=closed, height=height, wrap=wrap) 

366 

367 # get the azimuthal equidistant projection, once 

368 A = _Equidistant00(equidistant, c) 

369 b = _Box(c, c3.distance) 

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

371 

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

373 LatLon=self.classof, # this LatLon 

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

375 try: 

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

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

378 p2 = _unrollon(p1, p2) 

379 # skip edge if no overlap with box around closest 

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

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

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

383 if d3.distance < c3.distance: 

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

385 b = _Box(c, c3.distance) 

386 m = max(m, c.iteration) 

387 p1, i = p2, j 

388 

389 except (TypeError, ValueError) as x: 

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

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

392 closed=closed, height=height, wrap=wrap) 

393 

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

395 

396 n = typename(self) 

397 c.rename(n) 

398 if s is not c: 

399 s = s.copy(name=n) 

400 if e is not c: 

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

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

403 iteration=m) # ._iteration for tests 

404 

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

406 wrap=False, tol=_TOL): 

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

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

409 

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

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

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

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

414 see method L{Ellipsoid.geodesic_}. 

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

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

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

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

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

420 

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

422 

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

424 

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

426 ''' 

427 try: 

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

429 

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

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

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

433 p._iteration = P.iteration 

434 except (TypeError, ValueError) as x: 

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

436 exact=exact, wrap=wrap) 

437 return p 

438 

439 

440class _Box(object): 

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

442 

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

444 ''' 

445 _1_01 = _1_0 + _0_01 # ~1% margin 

446 

447 def __init__(self, center, distance): 

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

449 

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

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

452 (C{meter}, conventionally) 

453 ''' 

454 m = Radius_(distance=distance) 

455 E = center.ellipsoid() 

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

457 self._N = center.lat + d 

458 self._S = center.lat - d 

459 self._E = center.lon + d 

460 self._W = center.lon - d 

461 

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

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

464 

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

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

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

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

469 

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

471 otherwise (C{bool}). 

472 ''' 

473 if lat1 > lat2: 

474 lat1, lat2 = lat2, lat1 

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

476 return False 

477 if lon1 > lon2: 

478 lon1, lon2 = lon2, lon1 

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

480 return False 

481 return True 

482 

483 

484class _Tol(object): 

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

486 ''' 

487 _deg = 0 # tol in degrees 

488 _lat = 0 

489 _m = 0 # tol in meter 

490 _min = MAX # degrees 

491 _prev = None 

492 _r = 0 

493 

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

495 '''New L{_Tol}. 

496 

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

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

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

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

501 ''' 

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

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

504 self._m = max(EPS, tol_m) 

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

506 

507 @property_RO 

508 def degrees(self): 

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

510 ''' 

511 return self._deg 

512 

513 def degrees2m(self, deg): 

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

515 ''' 

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

517 

518 def degError(self, Error=_ValueError): 

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

520 ''' 

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

522 

523 def done(self, deg): 

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

525 ''' 

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

527 return True 

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

529 self._prev = deg 

530 return False 

531 

532 @property_RO 

533 def lat(self): 

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

535 ''' 

536 return self._lat 

537 

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

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

540 ''' 

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

542 if m2km(m) > self.meter: 

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

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

545 

546 @property_RO 

547 def meter(self): 

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

549 ''' 

550 return self._m 

551 

552 @property_RO 

553 def radius(self): 

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

555 ''' 

556 return self._r 

557 

558 def reset(self): 

559 '''Reset tolerances. 

560 ''' 

561 self._min = MAX # delattrof() 

562 self._prev = None # delattrof() 

563 

564 

565def _Equidistant00(equidistant, p1): 

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

567 ''' 

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

569 equidistant = p1.Equidistant 

570 else: 

571 _xsubclassof(*_MODS.azimuthal._Equidistants, 

572 equidistant=equidistant) 

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

574 

575 

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

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

578 or as a point and (forward) bearing. 

579 

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

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

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

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

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

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

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

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

588 L{intersecant2<LatLonEllipsoidalBaseDI.intersecant2>}. 

589 

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

591 

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

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

594 

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

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

597 ''' 

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

599 raise _IsnotError(_ellipsoidal_, center=center) 

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

601 

602 

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

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

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

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

607 ''' 

608 _LLS = _MODS.sphericalTrigonometry.LatLon 

609 _si = _MODS.sphericalTrigonometry._intersect 

610 _vi3 = _MODS.vector3d._intersect3d3 

611 

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

613 # compute opposing and distance 

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

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

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

617 return b, t.distance 

618 

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

620 # compute an end point along the initial bearing about 

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

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

623 # radians in .sphericalTrigonometry._int3d2 and bearing 

624 # comparison in .sphericaltrigonometry._intb 

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

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

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

628 e = s.destination(d, e) 

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

630 

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

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

633 ll = not _isDegrees(e) 

634 if ll: 

635 e = s.others(**end) 

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

637 e = _unrollon(s, e) 

638 return e, ll 

639 

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

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

642 if not o: # intersection may be on start 

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

644 return o 

645 return -n if b else n 

646 

647 E = s1.ellipsoids(s2) 

648 

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

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

651 

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

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

654 

655 # get the azimuthal equidistant projection 

656 A = _Equidistant00(equidistant, s1) 

657 

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

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

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

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

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

663 

664 if not ll1: 

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

666 if not ll2: 

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

668 

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

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

671 for i in range(1, _TRIPS): 

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

673 # convert start and end points to projection 

674 # space and compute an intersection there 

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

676 eps=e.meter, useZ=False) 

677 # convert intersection back to geodetic 

678 t, d = A._reverse2(v) 

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

680 break 

681 else: 

682 raise e.degError(Error=IntersectionError) 

683 

684 # like .sphericalTrigonometry._intersect, if this intersection 

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

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

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

688 if b1: 

689 t = t.antipodal() 

690 b1 = not b1 

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

692 

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

694 iteration=i, name=n) 

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

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

697 

698 

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

700 **equidistant_tol_LatLon_and_kwds): 

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

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

703 point and an initial bearing from North. 

704 ''' 

705 s1 = _xellipsoidal(start1=start1) 

706 s2 = s1.others(start2=start2) 

707 try: 

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

709 **equidistant_tol_LatLon_and_kwds) 

710 except (TypeError, ValueError) as x: 

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

712 

713 

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

715 **equidistant_tol_LatLon_and_kwds): 

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

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

718 ''' 

719 c1 = _xellipsoidal(center1=center1) 

720 c2 = c1.others(center2=center2) 

721 try: 

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

723 **equidistant_tol_LatLon_and_kwds) 

724 except (TypeError, ValueError) as x: 

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

726 center2=center2, radius2=radius2) 

727 

728 

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

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

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

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

733 ''' 

734 _LLS = _MODS.sphericalTrigonometry.LatLon 

735 _si2 = _MODS.sphericalTrigonometry._intersects2 

736 _vi2 = _MODS.vector3d._intersects2 

737 

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

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

740 iteration=t.iteration, name=n) 

741 

742 r1 = Radius_(radius1=radius1) 

743 r2 = Radius_(radius2=radius2) 

744 

745 E = c1.ellipsoids(c2) 

746 # get the azimuthal equidistant projection 

747 A = _Equidistant00(equidistant, c1) 

748 

749 if r1 < r2: 

750 c1, c2 = c2, c1 

751 r1, r2 = r2, r1 

752 

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

754 raise _ValueError(_exceed_PI_radians_) 

755 

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

757 c2 = _unrollon(c1, c2) 

758 

759 # distance between centers and radii are 

760 # measured along the ellipsoid's surface 

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

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

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

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

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

766 

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

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

769 

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

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

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

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

774 

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

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

777 ts, ta = [], None 

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

779 for i in range(1, _TRIPS): 

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

781 # convert centers to projection space and 

782 # compute the intersections there 

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

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

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

786 sphere=False, too_d=m) 

787 # convert intersections back to geodetic 

788 if v1 is v2: # abutting 

789 t, d = A._reverse2(v1) 

790 else: # consider the closer intersection 

791 t1, d1 = A._reverse2(v1) 

792 t2, d2 = A._reverse2(v2) 

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

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

795 t._iteration = i # _NamedTuple._iteration 

796 ts.append(t) 

797 if v1 is v2: # abutting 

798 ta = t 

799 break 

800 else: 

801 raise e.degError(Error=IntersectionError) 

802 e.reset() 

803 

804 if ta: # abutting circles 

805 pass # PYCHOK no cover 

806 elif len(ts) == 2: 

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

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

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

810 ta = ts[0] # assume abutting 

811 else: # PYCHOK no cover 

812 raise _AssertionError(ts=ts) 

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

814 return r, r 

815 

816 

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

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

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

820 separated to allow callers to embellish any exceptions. 

821 ''' 

822 p1 = p.others(point1=point1) 

823 p2 = p.others(point2=point2) 

824 

825 _ = p.ellipsoids(p1) 

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

827 

828 # get the azimuthal equidistant projection 

829 A = _Equidistant00(equidistant, p) 

830 

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

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

833 tol=tol, **LatLon_and_kwds) 

834 return NearestOn2Tuple(r, f) 

835 

836 

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

838 LatLon=None, **LatLon_kwds): 

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

840 _LLS = _MODS.sphericalNvector.LatLon 

841 _V3d = _MODS.vector3d.Vector3d 

842 _vn2 = _MODS.vector3d._nearestOn2 

843 

844 E = p.ellipsoids(p2) 

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

846 

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

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

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

850 height=height) 

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

852 if height is None: 

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

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

855 h0 = favg(h1, h2) 

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

857 h0 = h1 = h2 = _0_0 

858 

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

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

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

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

863 for i in range(1, _TRIPS): 

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

865 # convert points to projection space and compute 

866 # the nearest one (and its height) there 

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

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

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

870 # convert nearest one back to geodetic 

871 t, d = A._reverse2(v) 

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

873 break 

874 else: 

875 raise e.degError() 

876 

877 if height is None: 

878 h = v.z # nearest 

879 elif _isHeight(height): 

880 h = height 

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

882 iteration=i, name=n) 

883 return r, f, e # fraction or None 

884 

885 

886__all__ += _ALL_DOCS(LatLonEllipsoidalBaseDI, intersecant2) 

887del _1_0, _0_01 

888 

889# **) MIT License 

890# 

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

892# 

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

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

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

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

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

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

899# 

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

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

902# 

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

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

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

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

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

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

909# OTHER DEALINGS IN THE SOFTWARE.