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 _ # PYCHOK semicolon

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

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):

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

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

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

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

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

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

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

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

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

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

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

886 del _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.