1

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

3

4 u'''Spherical, C{N-vector}-based geodesy.

5

6 N-vector-based classes geodetic (lat-/longitude) L{LatLon}, geocentric

7(ECEF) L{Cartesian} and C{Nvector} and functions L{areaOf}, L{intersection},

8 L{meanOf}, L{nearestOn3}, L{perimeterOf}, L{sumOf}, L{triangulate} and

9 L{trilaterate}, I{all spherical}.

10

11 Pure Python implementation of n-vector-based spherical geodetic (lat-/longitude)

12 methods, transcoded from JavaScript originals by I{(C) Chris Veness 2011-2024},

13 published under the same MIT Licence**. See U{Vector-based geodesy

14<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>} and

15 U{Module latlon-nvector-spherical

16<https://www.Movable-Type.co.UK/scripts/geodesy/docs/module-latlon-nvector-spherical.html>}.

17

18 Tools for working with points and lines on (a spherical model of) the

19 earth’s surface using using n-vectors rather than the more common

20 spherical trigonometry. N-vectors make many calculations much simpler,

21 and easier to follow, compared with the trigonometric equivalents.

22

23 Based on Kenneth Gade’s U{‘Non-singular Horizontal Position Representation’

24<https://www.NavLab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf>},

25 The Journal of Navigation (2010), vol 63, nr 3, pp 395-417.

26

27 Note that the formulations below take x => 0°N,0°E, y => 0°N,90°E and

28 z => 90°N while Gade uses x => 90°N, y => 0°N,90°E, z => 0°N,0°E.

29

30 Also note that on a spherical earth model, an n-vector is equivalent

31 to a normalised version of an (ECEF) cartesian coordinate.

32'''

33# make sure int/int division yields float quosient, see .basics

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

35

36 from pygeodesy.basics import _isin, _xinstanceof, typename

37 from pygeodesy.constants import EPS, EPS0, PI, PI2, PI_2, R_M, \

38 _0_0, _0_5, _1_0

39# from pygeodesy.datums import Datums # from .sphericalBase

40 from pygeodesy.errors import PointsError, VectorError, _xError, _xkwds

41 from pygeodesy.fmath import fdot_, fmean, fsum

42# from pygeodesy.fsums import fsum # from .fmath

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

44 from pygeodesy.interns import _composite_, _end_, _Nv00_, _other_, \

45 _point_, _pole_

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

47# from pygeodesy.named import notImplemented # from .points

48# from pygeodesy.namedTuples import NearestOn3Tuple # from .points

49 from pygeodesy.nvectorBase import LatLonNvectorBase, NorthPole, _nsumOf, \

50 NvectorBase, _triangulate, _trilaterate

51 from pygeodesy.points import NearestOn3Tuple, notImplemented, \

52 ispolar # PYCHOK exported

53 from pygeodesy.props import deprecated_function, deprecated_method, \

54 property_RO

55 from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \

56 _intersecant2, LatLonSphericalBase, \

57 _radians2m, Datums

58 from pygeodesy.units import Bearing, Bearing_, _isDegrees, Radius, Scalar

59 from pygeodesy.utily import atan2, degrees360, sincos2, sincos2_, sincos2d, \

60 _unrollon, _Wrap, fabs

61

62# from math import fabs # from utily

63

64__all__ = _ALL_LAZY.sphericalNvector

65__version__ = '25.04.14'

66

67_lines_ = 'lines'

68

69

70 class Cartesian(CartesianSphericalBase):

71 '''Extended to convert geocentric, L{Cartesian} points to

72 C{Nvector} and n-vector-based, spherical L{LatLon}.

73 '''

74

75 def toLatLon(self, **LatLon_and_kwds): # PYCHOK LatLon=LatLon

76 '''Convert this cartesian to an C{Nvector}-based geodetic point.

77

78 @kwarg LatLon_and_kwds: Optional C{LatLon} class and C{LatLon} keyword

79 arguments, like C{datum}. Use C{B{LatLon}=...}

80 to override this L{LatLon} class or specify

81 C{B{LatLon}=None}.

82

83 @return: A C{LatLon} or if C{LatLon is None}, an L{Ecef9Tuple}C{(x, y, z,

84 lat, lon, height, C, M, datum)} with C{C} and C{M} if available.

85

86 @raise TypeError: Invalid C{LatLon} or other B{C{LatLon_and_kwds}} item.

87 '''

88 kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)

89 return CartesianSphericalBase.toLatLon(self, **kwds)

90

91 def toNvector(self, **Nvector_and_kwds): # PYCHOK Datums.WGS84

92 '''Convert this cartesian to C{Nvector} components, I{including height}.

93

94 @kwarg Nvector_and_kwds: Optional C{Nvector} class and C{Nvector} keyword

95 arguments, like C{datum}. Use C{B{Nvector}=...}

96 to override this C{Nvector} class or specify

97 C{B{Nvector}=None}.

98

99 @return: An C{Nvector}) or if C{Nvector is None}, a L{Vector4Tuple}C{(x, y, z, h)}.

100

101 @raise TypeError: Invalid C{Nvector} or other B{C{Nvector_and_kwds}} item.

102 '''

103 # ll = CartesianBase.toLatLon(self, LatLon=LatLon,

104 # datum=datum or self.datum)

105 # kwds = _xkwds(kwds, Nvector=Nvector)

106 # return ll.toNvector(**kwds)

107 kwds = _xkwds(Nvector_and_kwds, Nvector=Nvector, datum=self.datum)

108 return CartesianSphericalBase.toNvector(self, **kwds)

109

110

111 class LatLon(LatLonNvectorBase, LatLonSphericalBase):

112 '''New n-vector-based point on a spherical earth model.

113

114 Tools for working with points, lines and paths on (a spherical

115 model of) the earth's surface using vector-based methods.

116 '''

117 _Nv = None # cached_toNvector C{Nvector})

118

119 def _update(self, updated, *attrs, **setters): # PYCHOK args

120 '''(INTERNAL) Zap cached attributes if updated.

121 '''

122 if updated: # reset caches

123 LatLonNvectorBase._update(self, updated, _Nv=self._Nv) # special case

124 LatLonSphericalBase._update(self, updated, *attrs, **setters)

125

126 def alongTrackDistanceTo(self, start, end, radius=R_M, wrap=False):

127 '''Compute the (signed) distance from the start to the closest

128 point on the great circle line defined by a start and an

129 end point.

130

131 That is, if a perpendicular is drawn from this point to the

132 great circle line, the along-track distance is the distance

133 from the start point to the point where the perpendicular

134 crosses the line.

135

136 @arg start: Start point of great circle line (L{LatLon}).

137 @arg end: End point of great circle line (L{LatLon}) or

138 initial bearing from start point (compass

139 C{degrees360}).

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

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

142 the B{C{start}} and B{C{end}} points (C{bool}).

143

144 @return: Distance along the great circle line (C{radians}

145 if C{B{radius} is None} else C{meter}, same units

146 as B{C{radius}}), positive if "after" the start

147 toward the end point of the line or negative if

148 "before" the start point.

149

150 @raise TypeError: If B{C{start}} or B{C{end}} point is not L{LatLon}.

151

152 @raise Valuerror: Some points coincide.

153 '''

154 p = self.others(start=start)

155 n = self.toNvector()

156

157 gc, _, _ = self._gc3(p, end, _end_, wrap=wrap)

158 a = gc.cross(n).cross(gc) # along-track point gc × p × gc

159 return _radians2m(start.toNvector().angleTo(a, vSign=gc), radius)

160

161 @deprecated_method

162 def bearingTo(self, other, **unused): # PYCHOK no cover

163 '''DEPRECATED, use method L{initialBearingTo}.

164 '''

165 return self.initialBearingTo(other)

166

167 def crossTrackDistanceTo(self, start, end, radius=R_M, wrap=False):

168 '''Compute the (signed) distance from this point to great circle

169 defined by a start and end point.

170

171 @arg start: Start point of great circle line (L{LatLon}).

172 @arg end: End point of great circle line (L{LatLon}) or initial

173 bearing from start point (compass C{degrees360}).

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

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

176 B{C{start}} and B{C{end}} points (C{bool}).

177

178 @return: Distance to great circle (C{radians} if C{B{radius}

179 is None} else C{meter}, same units as B{C{radius}}),

180 negative if to the left or positive if to the right

181 of the line .

182

183 @raise TypeError: If B{C{start}} or B{C{end}} point is not L{LatLon}.

184

185 @raise Valuerror: Some points coincide.

186 '''

187 p = self.others(start=start)

188 n = self.toNvector()

189

190 gc, _, _ = self._gc3(p, end, _end_, wrap=wrap)

191 return _radians2m(gc.angleTo(n) - PI_2, radius)

192

193 def destination(self, distance, bearing, radius=R_M, height=None):

194 '''Locate the destination from this point after having travelled

195 the given distance on the given bearing.

196

197 @arg distance: Distance travelled (C{meter}, same units as

198 B{C{radius}}).

199 @arg bearing: Bearing from this point (compass C{degrees360}).

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

201 @kwarg height: Optional height at destination, overriding the

202 default height (C{meter}, same units as B{C{radius}}).

203

204 @return: Destination point (L{LatLon}).

205

206 @raise Valuerror: Polar coincidence or invalid B{C{distance}},

207 B{C{bearing}}, B{C{radius}} or B{C{height}}.

208 '''

209 b = Bearing_(bearing)

210 a = _m2radians(distance, radius, low=None)

211 sa, ca, sb, cb = sincos2_(a, b)

212

213 n = self.toNvector()

214 e = NorthPole.cross(n, raiser=_pole_).unit() # east vector at n

215 x = n.cross(e) # north vector at n

216 d = x.times(cb).plus(e.times(sb)) # direction vector @ n

217 n = n.times(ca).plus(d.times(sa))

218 return n.toLatLon(height=height, LatLon=self.classof) # Nvector(n.x, n.y, n.z).toLatLon(...)

219

220 def distanceTo(self, other, radius=R_M, wrap=False):

221 '''Compute the distance from this to an other point.

222

223 @arg other: The other point (L{LatLon}).

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

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

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

227

228 @return: Distance between this and the B{C{other}} point

229 (C{meter}, same units as B{C{radius}} or C{radians}

230 if C{B{radius} is None}).

231

232 @raise TypeError: Invalid B{C{other}} point.

233 '''

234 p = self.others(other)

235 if wrap:

236 p = _unrollon(self, p)

237 n = p.toNvector()

238 r = fabs(self.toNvector().angleTo(n, wrap=wrap))

239 return r if radius is None else (Radius(radius) * r)

240

241# @Property_RO

242# def Ecef(self):

243# '''Get the ECEF I{class} (L{EcefVeness}), I{lazily}.

244# '''

245# return _ALL_MODS.ecef.EcefKarney

246

247 def _gc3(self, start, end, namend, raiser=_point_, wrap=False):

248 '''(INTERNAL) Return great circle, start and end Nvectors.

249 '''

250 s = start.toNvector()

251 if _isDegrees(end): # bearing

252 gc = s.greatCircle(end)

253 e = None

254 else: # point

255 p = self.others(end, name=namend)

256 if wrap:

257 p = _unrollon(start, p, wrap=wrap)

258 e = p.toNvector()

259 gc = s.cross(e, raiser=raiser) # XXX .unit()?

260 return gc, s, e

261

262 def greatCircle(self, bearing):

263 '''Compute the vector normal to great circle obtained by

264 heading on the given bearing from this point.

265

266 Direction of vector is such that initial bearing vector

267 b = c × n, where n is an n-vector representing this point.

268

269 @arg bearing: Bearing from this point (compass C{degrees360}).

270

271 @return: N-vector representing the great circle (C{Nvector}).

272 '''

273 t = Bearing_(bearing)

274 a, b = self.philam

275

276 sa, ca, sb, cb, st, ct = sincos2_(a, b, t)

277

278 sa *= st

279 return Nvector(fdot_(sb, ct, -sa, cb),

280 -fdot_(cb, ct, sa, sb),

281 ca * st, name=self.name) # XXX .unit()

282

283 def greatCircleTo(self, other, wrap=False):

284 '''Compute the vector normal to great circle obtained by

285 heading from this to an other point or on a given bearing.

286

287 Direction of vector is such that initial bearing vector

288 b = c × n, where n is an n-vector representing this point.

289

290 @arg other: The other point (L{LatLon}) or the bearing from

291 this point (compass C{degrees360}).

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

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

294

295 @return: N-vector representing the great circle (C{Nvector}).

296

297 @raise TypeError: The B{C{other}} point is not L{LatLon}.

298

299 @raise Valuerror: Points coincide.

300 '''

301 gc, _, _ = self._gc3(self, other, _other_, wrap=wrap)

302 return gc.unit()

303

304 def initialBearingTo(self, other, wrap=False, **unused): # raiser=...

305 '''Compute the initial bearing (forward azimuth) from this

306 to an other point.

307

308 @arg other: The other point (L{LatLon}).

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

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

311

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

313

314 @raise Crosserror: This point coincides with the B{C{other}}

315 point or the C{NorthPole} and L{crosserrors

316 <pygeodesy.crosserrors>} is C{True}.

317

318 @raise TypeError: The B{C{other}} point is not L{LatLon}.

319 '''

320 n = self.toNvector()

321 p = self.others(other)

322 if wrap:

323 p = _unrollon(self, p, wrap=wrap)

324 p = p.toNvector()

325 # see <https://MathForum.org/library/drmath/view/55417.html>

326# gc1 = self.greatCircleTo(other)

327 gc1 = n.cross(p, raiser=_point_) # .unit()

328# gc2 = self.greatCircleTo(NorthPole)

329 gc2 = n.cross(NorthPole, raiser=_pole_) # .unit()

330 return degrees360(gc1.angleTo(gc2, vSign=n))

331

332 def intermediateChordTo(self, other, fraction, height=None, wrap=False):

333 '''Locate the point projected from the point at given fraction

334 on a straight line (chord) between this and an other point.

335

336 @arg other: The other point (L{LatLon}).

337 @arg fraction: Fraction between both points (float, between

338 0.0 for this and 1.0 for the other point).

339 @kwarg height: Optional height at the intermediate point,

340 overriding the fractional height (C{meter}).

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

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

343

344 @return: Intermediate point (L{LatLon}).

345

346 @raise TypeError: The B{C{other}} point is not L{LatLon}.

347 '''

348 n = self.toNvector()

349 p = self.others(other)

350 if wrap:

351 p = _unrollon(self, p, wrap=wrap)

352

353 f = Scalar(fraction=fraction)

354 i = p.toNvector().times(f).plus(n.times(1 - f))

355# i = p.toNvector() * f + self.toNvector() * (1 - f))

356

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

358 return i.toLatLon(height=h, LatLon=self.classof) # Nvector(i.x, i.y, i.z).toLatLon(...)

359

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

361 '''Locate the point at a given fraction between this and an

362 other point.

363

364 @arg other: The other point (L{LatLon}).

365 @arg fraction: Fraction between both points (C{float}, between

366 0.0 for this and 1.0 for the other point).

367 @kwarg height: Optional height at the intermediate point,

368 overriding the fractional height (C{meter}).

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

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

371

372 @return: Intermediate point (L{LatLon}).

373

374 @raise TypeError: The B{C{other}} point is not L{LatLon}.

375

376 @raise Valuerror: Points coincide or invalid B{C{height}}.

377

378 @see: Methods C{midpointTo} and C{rhumbMidpointTo}.

379 '''

380 n = self.toNvector()

381 p = self.others(other)

382 if wrap:

383 p = _unrollon(self, p, wrap=wrap)

384 p = p.toNvector()

385 f = Scalar(fraction=fraction)

386

387 x = n.cross(p, raiser=_point_)

388 d = x.unit().cross(n) # unit(n × p) × n

389 # angular distance α, tan(α) = |n × p| / n ⋅ p

390 s, c = sincos2(atan2(x.length, n.dot(p)) * f) # interpolated

391 i = n.times(c).plus(d.times(s)) # n * cosα + d * sinα

392

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

394 return i.toLatLon(height=h, LatLon=self.classof) # Nvector(i.x, i.y, i.z).toLatLon(...)

395

396 def intersection(self, end1, start2, end2, height=None, wrap=False):

397 '''Locate an intersection point of two lines each defined by two

398 points or by a point and an (initial) bearing.

399

400 @return: The intersection point (L{LatLon}).

401

402 @see: Method L{intersection2<sphericalNvector.LatLon.intersection2>}

403 for further details.

404 '''

405 return intersection(self, end1, start2, end2, height=height,

406 wrap=wrap, LatLon=self.classof)

407

408 def intersection2(self, end1, start2, end2, height=None, wrap=False):

409 '''Locate both intersections of two (great circle) lines each defined

410 by two points or by a point and an (initial) bearing.

411

412 @arg end1: End point of the line starting at this point (L{LatLon})

413 or the bearing at this point (compass C{degrees360}).

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

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

416 at B{C{start2}} (compass C{degrees360}).

417 @kwarg height: Optional height at the intersection and antipodal

418 point, overriding the mean height (C{meter}).

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

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

421

422 @return: 2-Tuple C{(intersection, antipode)}, each a B{C{LatLon}}.

423

424 @raise TypeError: If B{C{start2}}, B{C{end1}} or B{C{end2}}

425 point is not L{LatLon}.

426

427 @raise ValueError: Intersection is ambiguous or infinite or

428 the lines are parallel, coincident or null.

429

430 @see: Function L{sphericalNvector.intersection2}.

431 '''

432 return intersection2(self, end1, start2, end2, height=height,

433 wrap=wrap, LatLon=self.classof)

434

435 def isenclosedBy(self, points, wrap=False):

436 '''Check whether a (convex) polygon or composite encloses this point.

437

438 @arg points: The polygon points or composite (L{LatLon}[],

439 L{BooleanFHP} or L{BooleanGH}).

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

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

442

443 @return: C{True} if this point is inside the polygon or composite,

444 C{False} otherwise.

445

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

447

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

449

450 @see: Functions L{pygeodesy.isconvex}, L{pygeodesy.isenclosedBy}

451 and L{pygeodesy.ispolar} especially if the B{C{points}} may

452 enclose a pole or wrap around the earth I{longitudinally}.

453 '''

454 if _MODS.booleans.isBoolean(points):

455 return points._encloses(self.lat, self.lon, wrap=wrap)

456

457 # sum subtended angles of each edge (using n0, the

458 # normal vector to this point for sign of α)

459 def _subt(Ps, n0, w):

460 p1 = Ps[0]

461 vs1 = n0.minus(p1.toNvector())

462 for p2 in Ps.iterate(closed=True):

463 if w and not Ps.looped:

464 p2 = _unrollon(p1, p2)

465 p1 = p2

466 vs2 = n0.minus(p2.toNvector())

467 yield vs1.angleTo(vs2, vSign=n0) # PYCHOK false

468 vs1 = vs2

469

470 # Note, this method uses angle summation test: on a plane,

471 # angles for an enclosed point will sum to 360°, angles for

472 # an exterior point will sum to 0°. On a sphere, enclosed

473 # point angles will sum to less than 360° (due to spherical

474 # excess), exterior point angles will be small but non-zero.

475 s = fsum(_subt(self.PointsIter(points, loop=1, wrap=wrap),

476 self.toNvector(), wrap)) # normal vector

477 # XXX are winding number optimisations equally applicable to

478 # spherical surface?

479 return fabs(s) > PI

480

481 @deprecated_method

482 def isEnclosedBy(self, points): # PYCHOK no cover

483 '''DEPRECATED, use method C{isenclosedBy}.'''

484 return self.isenclosedBy(points)

485

486 def iswithin(self, point1, point2, wrap=False):

487 '''Check whether this point is between two other points.

488

489 If this point is not on the great circle arc defined by

490 both points, return whether it is within the area bound

491 by perpendiculars to the great circle at each point (in

492 the same hemispere).

493

494 @arg point1: Start point of the arc (L{LatLon}).

495 @arg point2: End point of the arc (L{LatLon}).

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

497 B{C{point1}} and B{C{point2}} (C{bool}).

498

499 @return: C{True} if this point is within the (great circle)

500 arc, C{False} otherwise.

501

502 @raise TypeError: If B{C{point1}} or B{C{point2}} is not

503 L{LatLon}.

504 '''

505 p1 = self.others(point1=point1)

506 p2 = self.others(point2=point2)

507 if wrap:

508 p1 = _Wrap.point(p1)

509 p2 = _unrollon(p1, p2, wrap=wrap)

510 n, n1, n2 = (_.toNvector() for _ in (self, p1, p2))

511

512 # corner case, null arc

513 if n1.isequalTo(n2):

514 return n.isequalTo(n1) or n.isequalTo(n2) # PYCHOK returns

515

516 if n.dot(n1) < 0 or n.dot(n2) < 0: # different hemisphere

517 return False # PYCHOK returns

518

519 # get vectors representing d0=p0->p1 and d2=p2->p1 and the

520 # dot product d0⋅d2 tells us if p0 is on the p2 side of p1 or

521 # on the other side (similarly for d0=p0->p2 and d1=p1->p2

522 # and dot product d0⋅d1 and p0 on the p1 side of p2 or not)

523 return n.minus(n1).dot(n2.minus(n1)) >= 0 and \

524 n.minus(n2).dot(n1.minus(n2)) >= 0

525

526 @deprecated_method

527 def isWithin(self, point1, point2): # PYCHOK no cover

528 '''DEPRECATED, use method C{iswithin}.'''

529 return self.iswithin(point1, point2)

530

531 def midpointTo(self, other, height=None, fraction=_0_5, wrap=False):

532 '''Find the midpoint between this and an other point.

533

534 @arg other: The other point (L{LatLon}).

535 @kwarg height: Optional height at the midpoint, overriding

536 the mean height (C{meter}).

537 @kwarg fraction: Midpoint location from this point (C{scalar}),

538 may be negative or greater than 1.0.

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

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

541

542 @return: Midpoint (L{LatLon}).

543

544 @raise TypeError: The B{C{other}} point is not L{LatLon}.

545

546 @see: Methods C{intermediateTo} and C{rhumbMidpointTo}.

547 '''

548 if fraction is _0_5:

549 p = self.others(other)

550 if wrap:

551 p = _unrollon(self, p, wrap=wrap)

552 m = self.toNvector().plus(p.toNvector())

553 h = self._havg(other, f=fraction, h=height)

554 r = m.toLatLon(height=h, LatLon=self.classof)

555 else:

556 r = self.intermediateTo(other, fraction, height=height, wrap=wrap)

557 return r

558

559 def nearestOn(self, point1, point2, height=None, within=True, wrap=False):

560 '''Locate the point on the great circle arc between two points

561 closest to this point.

562

563 @arg point1: Start point of the arc (L{LatLon}).

564 @arg point2: End point of the arc (L{LatLon}).

565 @kwarg height: Optional height, overriding the mean height for

566 the point within the arc (C{meter}), or C{None}

567 to interpolate the height.

568 @kwarg within: If C{True}, return the closest point between both

569 given points, otherwise the closest point

570 elsewhere on the great circle arc (C{bool}).

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

572 B{C{point1}} and B{C{point2}} (C{bool}).

573

574 @return: Closest point on the arc (L{LatLon}).

575

576 @raise NotImplementedError: Keyword argument C{B{wrap}=True}

577 not supported.

578

579 @raise TypeError: Invalid B{C{point1}} or B{C{point2}}.

580 '''

581 p1 = self.others(point1=point1)

582 p2 = self.others(point2=point2)

583 if wrap:

584 p1 = _Wrap.point(p1)

585 p2 = _unrollon(p1, p2, wrap=wrap)

586 p0 = self

587

588 if p0.iswithin(p1, p2) and not p1.isequalTo(p2, EPS):

589 # closer to arc than to its endpoints,

590 # find the closest point on the arc

591 gc1 = p1.toNvector().cross(p2.toNvector())

592 gc2 = p0.toNvector().cross(gc1)

593 n = gc1.cross(gc2)

594

595 elif within: # for backward compatibility, XXX unwrapped

596 return point1 if (self.distanceTo(point1) <

597 self.distanceTo(point2)) else point2

598

599 else: # handle beyond arc extent by .vector3d.nearestOn

600 n1 = p1.toNvector()

601 n2 = p2.toNvector()

602 n = p0.toNvector().nearestOn(n1, n2, within=False)

603 if n is n1:

604 return p1 # is point1

605 elif n is n2:

606 return p2 # is point2 if not wrap

607

608 p = n.toLatLon(height=height or 0, LatLon=self.classof)

609 if _isin(height, None, False): # interpolate height within extent

610 d = p1.distanceTo(p2)

611 f = (p1.distanceTo(p) / d) if d > EPS0 else _0_5

612 p.height = p1._havg(p2, f=max(_0_0, min(f, _1_0)))

613 return p

614

615 # @deprecated_method

616 def nearestOn2(self, points, **closed_radius_height): # PYCHOK no cover

617 '''DEPRECATED, use method L{sphericalNvector.LatLon.nearestOn3}.

618

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

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

621 to that point from this point ...

622 '''

623 r = self.nearestOn3(points, **closed_radius_height)

624 return r.closest, r.distance

625

626 def nearestOn3(self, points, closed=False, radius=R_M, height=None, wrap=False):

627 '''Locate the point on a path or polygon (with great circle arcs

628 joining consecutive points) closest to this point.

629

630 The closest point is either on within the extent of any great

631 circle arc or the nearest of the arc's end points.

632

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

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

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

636 @kwarg height: Optional height, overriding the mean height

637 for a point within the arc (C{meter}).

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

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

640

641 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of

642 the C{closest} point (L{LatLon}), the C{distance}

643 between this and the C{closest} point in C{meter},

644 same units as B{C{radius}} (or in C{radians} if

645 C{B{radius} is None}) and the C{angle} from this to

646 the C{closest} point in compass C{degrees360}.

647

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

649

650 @raise ValueError: No B{C{points}}.

651 '''

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

653 _d = self.distanceTo

654 _n = self.nearestOn

655

656 c = p1 = Ps[0]

657 r = _d(c, radius=None) # radians

658 for p2 in Ps.iterate(closed=closed):

659 if wrap and not Ps.looped:

660 p2 = _unrollon(p1, p2)

661 p = _n(p1, p2, height=height)

662 d = _d(p, radius=None) # radians

663 if d < r:

664 c, r = p, d

665 p1 = p2

666 d = r if radius is None else (Radius(radius) * r)

667 return NearestOn3Tuple(c, d, degrees360(r))

668

669 def toCartesian(self, **Cartesian_and_kwds): # PYCHOK Cartesian=Cartesian, datum=None

670 '''Convert this point to C{Nvector}-based cartesian (ECEF) coordinates.

671

672 @kwarg Cartesian_and_kwds: Optional L{Cartesian} and L{Cartesian} keyword

673 arguments, like C{datum}. Use C{B{Cartesian}=...}

674 to override this L{Cartesian} class or specify

675 C{B{Cartesian}=None}.

676

677 @return: A L{Cartesian} or if C{B{Cartesian} is None}, an L{Ecef9Tuple}C{(x, y,

678 z, lat, lon, height, C, M, datum)} with C{C} and C{M} if available.

679

680 @raise TypeError: Invalid L{Cartesian} or other B{C{Cartesian_and_kwds}} item.

681 '''

682 kwds = _xkwds(Cartesian_and_kwds, Cartesian=Cartesian, datum=self.datum)

683 return LatLonSphericalBase.toCartesian(self, **kwds)

684

685 def toNvector(self, **Nvector_and_kwds): # PYCHOK signature

686 '''Convert this point to C{Nvector} components, I{including height}.

687

688 @kwarg Nvector_and_kwds: Optional C{Nvector} and C{Nvector} keyword arguments.

689 Specify C{B{Nvector}=...} to override this C{Nvector}

690 class or use C{B{Nvector}=None}.

691

692 @return: An C{Nvector} or if C{B{Nvector} is None}, a L{Vector4Tuple}C{(x, y, z, h)}.

693

694 @raise TypeError: Invalid C{Nvector} or other B{C{Nvector_and_kwds}} item.

695 '''

696 return LatLonNvectorBase.toNvector(self, **_xkwds(Nvector_and_kwds, Nvector=Nvector))

697

698

699 class Nvector(NvectorBase):

700 '''An n-vector is a position representation using a (unit) vector

701 normal to the earth's surface. Unlike lat-/longitude points,

702 n-vectors have no singularities or discontinuities.

703

704 For many applications, n-vectors are more convenient to work

705 with than other position representations like lat-/longitude,

706 earth-centred earth-fixed (ECEF) vectors, UTM coordinates, etc.

707

708 On a spherical model earth, an n-vector is equivalent to an

709 earth-centred earth-fixed (ECEF) vector.

710

711 Note commonality with L{pygeodesy.ellipsoidalNvector.Nvector}.

712 '''

713 _datum = Datums.Sphere # default datum (L{Datum})

714

715 @property_RO

716 def sphericalNvector(self):

717 '''Get this C{Nvector}'s spherical class.

718 '''

719 return type(self)

720

721 def toCartesian(self, **Cartesian_and_kwds): # PYCHOK Cartesian=Cartesian

722 '''Convert this n-vector to C{Nvector}-based cartesian

723 (ECEF) coordinates.

724

725 @kwarg Cartesian_and_kwds: Optional L{Cartesian} and L{Cartesian} keyword

726 arguments, like C{h}. Use C{B{Cartesian}=...}

727 to override this L{Cartesian} class or specify

728 C{B{Cartesian}=None}.

729

730 @return: The cartesian point (L{Cartesian}) or if B{C{Cartesian}} is

731 set to C{None}, an L{Ecef9Tuple}C{(x, y, z, lat, lon, height,

732 C, M, datum)} with C{C} and C{M} if available.

733

734 @raise TypeError: Invalid B{C{Cartesian_and_kwds}} argument.

735 '''

736 kwds = _xkwds(Cartesian_and_kwds, h=self.h, Cartesian=Cartesian)

737 return NvectorBase.toCartesian(self, **kwds) # class or .classof

738

739 def toLatLon(self, **LatLon_and_kwds): # PYCHOK height=None, LatLon=LatLon

740 '''Convert this n-vector to an C{Nvector}-based geodetic point.

741

742 @kwarg LatLon_and_kwds: Optional L{LatLon} and L{LatLon} keyword

743 arguments, like C{height}. Use C{B{LatLon}=...}

744 to override this L{LatLon} class or specify

745 C{B{LatLon}=None}.

746

747 @return: The geodetic point (L{LatLon}) or if B{C{LatLon}} is set

748 to C{None}, an L{Ecef9Tuple}C{(x, y, z, lat, lon, height,

749 C, M, datum)} with C{C} and C{M} if available.

750

751 @raise TypeError: Invalid B{C{LatLon_and_kwds}} argument.

752

753 @raise ValueError: Invalid B{C{height}}.

754 '''

755 kwds = _xkwds(LatLon_and_kwds, height=self.h, LatLon=LatLon)

756 return NvectorBase.toLatLon(self, **kwds) # class or .classof

757

758 def greatCircle(self, bearing):

759 '''Compute the n-vector normal to great circle obtained by

760 heading on given (initial) bearing from this point as its

761 n-vector.

762

763 Direction of vector is such that initial bearing vector

764 b = c × p.

765

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

767

768 @return: N-vector representing great circle (C{Nvector}).

769

770 @raise Valuerror: Polar coincidence.

771 '''

772 s, c = sincos2d(Bearing(bearing))

773

774 e = NorthPole.cross(self, raiser=_pole_) # easting

775 n = self.cross(e, raiser=_point_) # northing

776

777 e = e.times(c / e.length)

778 n = n.times(s / n.length)

779 return n.minus(e)

780

781

782_Nv00 = LatLon(_0_0, _0_0, name=_Nv00_) # reference instance (L{LatLon})

783

784

785 def areaOf(points, radius=R_M, wrap=False):

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

787 great circle arcs joining consecutive points).

788

789 @arg points: The polygon points or clips (C{LatLon}[],

790 L{BooleanFHP} or L{BooleanGH}).

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

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

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

794

795 @return: Polygon area (C{meter} I{squared}, same units as

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

797

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

799

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

801

802 @see: Functions L{pygeodesy.areaOf}, L{sphericalTrigonometry.areaOf}

803 and L{ellipsoidalKarney.areaOf}.

804 '''

805 def _interangles(ps, w): # like .karney._polygon

806 Ps = _Nv00.PointsIter(ps, loop=2, wrap=w)

807 # use vector to 1st point as plane normal for sign of α

808 n0 = Ps[0].toNvector()

809

810 v2 = Ps[0]._N_vector # XXX v2 == no?

811 p1 = Ps[1]

812 v1 = p1._N_vector

813 gc = v2.cross(v1)

814 for p2 in Ps.iterate(closed=True):

815 if w and not Ps.looped:

816 p2 = _unrollon(p1, p2)

817 p1 = p2

818 v2 = p2._N_vector

819 gc1 = v1.cross(v2)

820 v1 = v2

821 yield gc.angleTo(gc1, vSign=n0)

822 gc = gc1

823

824 if _MODS.booleans.isBoolean(points):

825 r = points._sum2(LatLon, areaOf, radius=None, wrap=wrap)

826 else:

827 # sum interior angles: depending on whether polygon is cw or ccw,

828 # angle between edges is π−α or π+α, where α is angle between

829 # great-circle vectors; so sum α, then take n·π − |Σα| (cannot

830 # use Σ(π−|α|) as concave polygons would fail)

831 s = fsum(_interangles(points, wrap))

832 # using Girard’s theorem: A = [Σθᵢ − (n−2)·π]·R²

833 # (PI2 - abs(s) == (n*PI - abs(s)) - (n-2)*PI)

834 r = fabs(PI2 - fabs(s))

835 return r if radius is None else (r * Radius(radius)**2)

836

837

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

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

840 two points or as a point and bearing.

841

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

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

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

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

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

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

848 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method

849 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase.

850 intersecant2>} for further details.

851

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

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

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

855

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

857

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

859 not L{LatLon}.

860

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

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

863 '''

864 c = _Nv00.others(center=center)

865 p = _Nv00.others(point=point)

866 try:

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

868 except (TypeError, ValueError) as x:

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

870 **radius_exact_height_wrap)

871

872

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

874 **LatLon_and_kwds):

875 '''Locate an intersection point of two (great circle) lines each defined

876 by two points or by a point and an (initial) bearing.

877

878 @return: The intersection point (L{LatLon}) or if C{B{LatLon}=None},

879 a cartesian L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M,

880 datum)} with C{C} and C{M} if available.

881

882 @see: Function L{intersection2<sphericalNvector.intersection2>}

883 for further details.

884 '''

885 i, _, h = _intersect3(start1, end1, start2, end2, height, wrap)

886 kwds = _xkwds(LatLon_and_kwds, height=h, LatLon=LatLon)

887 return i.toLatLon(**kwds)

888

889

890 def intersection2(start1, end1, start2, end2, height=None, wrap=False,

891 **LatLon_and_kwds):

892 '''Locate both intersections of two (great circle) lines each defined

893 by two points or by a point and an (initial) bearing.

894

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

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

897 B{C{start1}} (compass C{degrees360}).

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

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

900 B{C{start2}} (compass C{degrees360}).

901 @kwarg height: Optional height at the intersection and antipodal point,

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

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

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

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

906 the intersection points and optionally, additional B{C{LatLon}}

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

908

909 @return: 2-Tuple C{(intersection, antipode)}, each a (B{C{LatLon}}) or if

910 C{B{LatLon}=None}, a cartesian L{Ecef9Tuple}C{(x, y, z, lat, lon,

911 height, C, M, datum)} with C{C} and C{M} if available.

912

913 @raise TypeError: If B{C{start*}} or B{C{end*}} is not L{LatLon}.

914

915 @raise ValueError: Intersection is ambiguous or infinite or the lines are

916 parallel, coincident or null.

917

918 @see: Function L{sphericalNvector.intersection}.

919 '''

920 i, a, h = _intersect3(start1, end1, start2, end2, height, wrap)

921 kwds = _xkwds(LatLon_and_kwds, height=h, LatLon=LatLon)

922 return i.toLatLon(**kwds), a.toLatLon(**kwds)

923

924

925 def _intersect3(start1, end1, start2, end2, height, wrap):

926 '''(INTERNAL) Return the intersection and antipodal points for

927 functions C{intersection} and C{intersection2}.

928 '''

929 p1 = _Nv00.others(start1=start1)

930 p2 = _Nv00.others(start2=start2)

931 if wrap:

932 p2 = _unrollon(p1, p2, wrap=wrap)

933 # If gc1 and gc2 are great circles through start and end points

934 # (or defined by start point and bearing), then the candidate

935 # intersections are simply gc1 × gc2 and gc2 × gc1. Most of the

936 # work is deciding the correct intersection point to select! If

937 # bearing is given, that determines the intersection, but if both

938 # lines are defined by start/end points, take closer intersection.

939 gc1, s1, e1 = _Nv00._gc3(p1, end1, 'end1', wrap=wrap)

940 gc2, s2, e2 = _Nv00._gc3(p2, end2, 'end2', wrap=wrap)

941

942 hs = start1.height, start2.height

943 # there are two (antipodal) candidate intersection

944 # points ... we have to choose the one to return

945 i1 = gc1.cross(gc2, raiser=_lines_)

946 i2 = gc2.cross(gc1, raiser=_lines_)

947

948 # selection of intersection point depends on how

949 # lines are defined (by bearings or endpoints)

950 if e1 and e2: # endpoint+endpoint

951 d = sumOf((s1, s2, e1, e2)).dot(i1)

952 hs += end1.height, end2.height

953 elif e1 and not e2: # endpoint+bearing

954 # gc2 x v2 . i1 +ve means v2 bearing points to i1

955 d = gc2.cross(s2).dot(i1)

956 hs += end1.height,

957 elif e2 and not e1: # bearing+endpoint

958 # gc1 x v1 . i1 +ve means v1 bearing points to i1

959 d = gc1.cross(s1).dot(i1)

960 hs += end2.height,

961 else: # bearing+bearing

962 # if gc x v . i1 is +ve, initial bearing is

963 # towards i1, otherwise towards antipodal i2

964 d1 = gc1.cross(s1).dot(i1) # +ve means p1 bearing points to i1

965 d2 = gc2.cross(s2).dot(i1) # +ve means p2 bearing points to i1

966 if d1 > 0 and d2 > 0:

967 d = 1 # both point to i1

968 elif d1 < 0 and d2 < 0:

969 d = -1 # both point to i2

970 else: # d1, d2 opposite signs

971 # intersection is at further-away intersection point,

972 # take opposite intersection from mid- point of v1

973 # and v2 [is this always true?] XXX changed to always

974 # get intersection p1 bearing points to, aka being

975 # located "after" p1 along the bearing at p1, like

976 # function .sphericalTrigonometry._intersect and

977 # .ellipsoidalBaseDI._intersect3

978 d = d1 # neg(s1.plus(s2).dot(i1))

979

980 h = fmean(hs) if height is None else height

981 return (i1, i2, h) if d > 0 else (i2, i1, h)

982

983

984 def meanOf(points, height=None, wrap=False, **LatLon_and_kwds):

985 '''Compute the I{geographic} mean of the supplied points.

986

987 @arg points: Array of points to be averaged (L{LatLon}[]).

988 @kwarg height: Optional height, overriding the mean height (C{meter}).

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

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

991 the mean point and optionally, additional B{C{LatLon}}

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

993

994 @return: Point at geographic mean and mean height (B{C{LatLon}}).

995

996 @raise PointsError: Insufficient number of B{C{points}} or some

997 B{C{points}} are not C{LatLon}.

998 '''

999 def _N_vs(ps, w):

1000 Ps = _Nv00.PointsIter(ps, wrap=w)

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

1002 yield p._N_vector

1003

1004 try:

1005 # geographic mean

1006 n = _nsumOf(_N_vs(points, wrap), height, Nvector, {})

1007 except (TypeError, ValueError) as x:

1008 raise PointsError(points=points, wrap=wrap, cause=x, **LatLon_and_kwds)

1009 return n.toLatLon(**_xkwds(LatLon_and_kwds, LatLon=LatLon, height=n.h,

1010 name=typename(meanOf)))

1011

1012

1013@deprecated_function

1014 def nearestOn2(point, points, **closed_radius_height): # PYCHOK no cover

1015 '''DEPRECATED, use method L{sphericalNvector.nearestOn3}.

1016

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

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

1019 between the C{closest} and the given B{C{point}} ...

1020 '''

1021 r = nearestOn3(point, points, **closed_radius_height)

1022 return r.closest, r.distance

1023

1024

1025 def nearestOn3(point, points, closed=False, radius=R_M, height=None, wrap=False):

1026 '''Locate the point on a polygon (with great circle arcs joining

1027 consecutive points) closest to an other point.

1028

1029 If the given point is between the end points of a great circle

1030 arc, the closest point is on that arc. Otherwise, the closest

1031 point is the nearest of the arc's end points.

1032

1033 @arg point: The other, reference point (L{LatLon}).

1034 @arg points: The polygon points (L{LatLon}[]).

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

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

1037 @kwarg height: Optional height, overriding the mean height for

1038 a point within the (great circle) arc (C{meter}).

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

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

1041

1042 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of

1043 the C{closest} point (L{LatLon}) on the polygon, the

1044 C{distance} and the C{angle} between the C{closest}

1045 and the given B{C{point}}. The C{distance} is in

1046 C{meter}, same units as B{C{radius}} or in C{radians}

1047 if C{B{radius} is None}, the C{angle} is in compass

1048 C{degrees360}.

1049

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

1051

1052 @raise TypeError: Some B{C{points}} or B{C{point}} not C{LatLon}.

1053 '''

1054 _xinstanceof(LatLon, point=point)

1055

1056 return point.nearestOn3(points, closed=closed, radius=radius,

1057 height=height, wrap=wrap)

1058

1059

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

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

1062 great circle arcs joining consecutive points).

1063

1064 @arg points: The polygon points (L{LatLon}[]).

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

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

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

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

1069

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

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

1072

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

1074

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

1076

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

1078 C{B{points}} a composite.

1079

1080 @see: Functions L{pygeodesy.perimeterOf}, L{ellipsoidalKarney.perimeterOf}

1081 and L{sphericalTrigonometry.perimeterOf}.

1082 '''

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

1084 Ps = _Nv00.PointsIter(ps, loop=1, wrap=w)

1085 p1 = Ps[0]

1086 v1 = p1._N_vector

1087 for p2 in Ps.iterate(closed=c):

1088 if w and not (c and Ps.looped):

1089 p2 = _unrollon(p1, p2)

1090 p1 = p2

1091 v2 = p2._N_vector

1092 yield v1.angleTo(v2)

1093 v1 = v2

1094

1095 if _MODS.booleans.isBoolean(points):

1096 if not closed:

1097 notImplemented(None, closed=closed, points=_composite_)

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

1099 else:

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

1101 return r if radius is None else (Radius(radius) * r)

1102

1103

1104 def sumOf(nvectors, Vector=Nvector, h=None, **Vector_kwds):

1105 '''Return the I{vectorial} sum of two or more n-vectors.

1106

1107 @arg nvectors: Vectors to be added (C{Nvector}[]).

1108 @kwarg Vector: Optional class for the vectorial sum (C{Nvector}).

1109 @kwarg h: Optional height, overriding the mean height (C{meter}).

1110 @kwarg Vector_kwds: Optional, additional B{C{Vector}} keyword arguments.

1111

1112 @return: Vectorial sum (B{C{Vector}}).

1113

1114 @raise VectorError: No B{C{nvectors}}.

1115 '''

1116 try:

1117 return _nsumOf(nvectors, h, Vector, Vector_kwds)

1118 except (TypeError, ValueError) as x:

1119 raise VectorError(nvectors=nvectors, Vector=Vector, cause=x)

1120

1121

1122 def triangulate(point1, bearing1, point2, bearing2,

1123 height=None, wrap=False,

1124 LatLon=LatLon, **LatLon_kwds):

1125 '''Locate a point given two known, reference points and the (initial)

1126 bearing from those points.

1127

1128 @arg point1: First reference point (L{LatLon}).

1129 @arg bearing1: Bearing at the first point (compass C{degrees360}).

1130 @arg point2: Second reference point (L{LatLon}).

1131 @arg bearing2: Bearing at the second point (compass C{degrees360}).

1132 @kwarg height: Optional height at the triangulated point, overriding

1133 the mean height (C{meter}).

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

1135 (C{bool}).

1136 @kwarg LatLon: Optional class to return the triangulated point (L{LatLon}).

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

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

1139

1140 @return: Triangulated point (B{C{LatLon}}).

1141

1142 @raise TypeError: If B{C{point1}} or B{C{point2}} is not L{LatLon}.

1143

1144 @raise Valuerror: Points coincide.

1145 '''

1146 return _triangulate(_Nv00.others(point1=point1), bearing1,

1147 _Nv00.others(point2=point2), bearing2,

1148 height=height, wrap=wrap,

1149 LatLon=LatLon, **LatLon_kwds)

1150

1151

1152 def trilaterate(point1, distance1, point2, distance2, point3, distance3, # PYCHOK args

1153 radius=R_M, height=None, useZ=False, wrap=False,

1154 LatLon=LatLon, **LatLon_kwds):

1155 '''Locate a point at given distances from three other points.

1156

1157 @arg point1: First point (L{LatLon}).

1158 @arg distance1: Distance to the first point (C{meter}, same units

1159 as B{C{radius}}).

1160 @arg point2: Second point (L{LatLon}).

1161 @arg distance2: Distance to the second point (C{meter}, same units

1162 as B{C{radius}}).

1163 @arg point3: Third point (L{LatLon}).

1164 @arg distance3: Distance to the third point (C{meter}, same units

1165 as B{C{radius}}).

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

1167 @kwarg height: Optional height at the trilaterated point, overriding

1168 the IDW height (C{meter}, same units as B{C{radius}}).

1169 @kwarg useZ: Include Z component iff non-NaN, non-zero (C{bool}).

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

1171 and B{C{point3}} (C{bool}).

1172 @kwarg LatLon: Optional class to return the trilaterated point (L{LatLon}).

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

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

1175

1176 @return: Trilaterated point (B{C{LatLon}}).

1177

1178 @raise IntersectionError: No intersection, trilateration failed.

1179

1180 @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.

1181

1182 @raise ValueError: Coincident B{C{points}} or invalid B{C{distance1}},

1183 B{C{distance2}}, B{C{distance3}} or B{C{radius}}.

1184

1185 @see: U{Trilateration<https://WikiPedia.org/wiki/Trilateration>}.

1186 '''

1187 return _trilaterate(_Nv00.others(point1=point1), distance1,

1188 _Nv00.others(point2=point2), distance2,

1189 _Nv00.others(point3=point3), distance3,

1190 radius=radius, height=height, useZ=useZ,

1191 wrap=wrap, LatLon=LatLon, **LatLon_kwds)

1192

1193

1194__all__ += _ALL_OTHER(Cartesian, LatLon, Nvector, # classes

1195 areaOf, # functions

1196 intersecant2, intersection, intersection2, ispolar,

1197 meanOf,

1198 nearestOn2, nearestOn3,

1199 perimeterOf,

1200 sumOf,

1201 triangulate, trilaterate)

1202

1203# **) MIT License

1204#

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

1206#

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

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

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

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

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

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

1213#

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

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

1216#

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

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

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

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

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

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

1223# OTHER DEALINGS IN THE SOFTWARE.