Coverage for pygeodesy/ellipsoidalBaseDI.py: 91%

1

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

3

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

5 class C{LatLonEllipsoidalBaseDI} and functions.

6'''

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

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

9

10# from pygeodesy.azimuthal import _Equidistants # _MODS

11 from pygeodesy.basics import isLatLon, _xsubclassof, typename

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

15 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase, _TOL_M, property_RO

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

17 _ValueError, _xellipsoidal, _xError, _xkwds_not

18 from pygeodesy.fmath import favg, fmean_

19 from pygeodesy.fsums import Fmt, fsumf_

20 from pygeodesy.formy import _isequalTo, opposing, _radical2

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

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

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

24 _exceed_PI_radians_, _near_, _SPACE_, _too_

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

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

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

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

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

35

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

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

440 class _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

484 class _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

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

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

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

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

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

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

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

837 def _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)

887 del _1_0, _0_01

888

889# **) MIT License

890#

891# Copyright (C) 2016-2026 -- 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.