Coverage for pygeodesy/sphericalNvector.py: 91%

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 _ # noqa: E702 ;

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

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, polar=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}[], L{BooleanFHP}

790 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 B{C{points}}

793 (C{bool}).

794 @kwarg polar: Use of C{B{polar}=True} not implemented (C{bool}).

795

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

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

798

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

800

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

802

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

804 and L{ellipsoidalKarney.areaOf}.

805 '''

806 if polar:

807 notImplemented(None, polar=polar, up=2)

808

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

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

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

812 n0 = Ps[0].toNvector()

813

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

815 p1 = Ps[1]

816 v1 = p1._N_vector

817 gc = v2.cross(v1)

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

819 if w and not Ps.looped:

820 p2 = _unrollon(p1, p2)

821 p1 = p2

822 v2 = p2._N_vector

823 gc1 = v1.cross(v2)

824 v1 = v2

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

826 gc = gc1

827

828 if _MODS.booleans.isBoolean(points):

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

830 else:

831 # sum interior angles: depending on whether polygon is cw or

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

833 # between great-circle vectors; so sum α, then take n·π − |Σα|

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

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

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

837 r = fabs(PI2 - fabs(s)) # == n*PI - abs(s) - (n-2)*PI

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

839

840

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

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

843 two points or as a point and bearing.

844

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

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

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

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

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

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

851 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method

852 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase.

853 intersecant2>} for further details.

854

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

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

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

858

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

860

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

862 not L{LatLon}.

863

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

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

866 '''

867 c = _Nv00.others(center=center)

868 p = _Nv00.others(point=point)

869 try:

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

871 except (TypeError, ValueError) as x:

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

873 **radius_exact_height_wrap)

874

875

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

877 **LatLon_and_kwds):

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

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

880

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

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

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

884

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

886 for further details.

887 '''

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

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

890 return i.toLatLon(**kwds)

891

892

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

894 **LatLon_and_kwds):

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

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

897

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

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

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

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

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

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

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

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

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

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

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

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

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

911

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

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

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

915

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

917

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

919 parallel, coincident or null.

920

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

922 '''

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

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

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

926

927

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

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

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

931 '''

932 p1 = _Nv00.others(start1=start1)

933 p2 = _Nv00.others(start2=start2)

934 if wrap:

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

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

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

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

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

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

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

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

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

944

945 hs = start1.height, start2.height

946 # there are two (antipodal) candidate intersection

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

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

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

950

951 # selection of intersection point depends on how

952 # lines are defined (by bearings or endpoints)

953 if e1 and e2: # endpoint+endpoint

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

955 hs += end1.height, end2.height

956 elif e1 and not e2: # endpoint+bearing

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

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

959 hs += end1.height,

960 elif e2 and not e1: # bearing+endpoint

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

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

963 hs += end2.height,

964 else: # bearing+bearing

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

966 # towards i1, otherwise towards antipodal i2

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

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

969 if d1 > 0 and d2 > 0:

970 d = 1 # both point to i1

971 elif d1 < 0 and d2 < 0:

972 d = -1 # both point to i2

973 else: # d1, d2 opposite signs

974 # intersection is at further-away intersection point,

975 # take opposite intersection from mid- point of v1

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

977 # get intersection p1 bearing points to, aka being

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

979 # function .sphericalTrigonometry._intersect and

980 # .ellipsoidalBaseDI._intersect3

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

982

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

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

985

986

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

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

989

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

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

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

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

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

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

996

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

998

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

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

1001 '''

1002 def _N_vs(ps, w):

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

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

1005 yield p._N_vector

1006

1007 try:

1008 # geographic mean

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

1010 except (TypeError, ValueError) as x:

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

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

1013 name=typename(meanOf)))

1014

1015

1016@deprecated_function

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

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

1019

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

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

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

1023 '''

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

1025 return r.closest, r.distance

1026

1027

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

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

1030 consecutive points) closest to an other point.

1031

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

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

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

1035

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

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

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

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

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

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

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

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

1044

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

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

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

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

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

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

1051 C{degrees360}.

1052

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

1054

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

1056 '''

1057 _xinstanceof(LatLon, point=point)

1058

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

1060 height=height, wrap=wrap)

1061

1062

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

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

1065 great circle arcs joining consecutive points).

1066

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

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

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

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

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

1072

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

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

1075

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

1077

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

1079

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

1081 C{B{points}} a composite.

1082

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

1084 and L{sphericalTrigonometry.perimeterOf}.

1085 '''

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

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

1088 p1 = Ps[0]

1089 v1 = p1._N_vector

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

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

1092 p2 = _unrollon(p1, p2)

1093 p1 = p2

1094 v2 = p2._N_vector

1095 yield v1.angleTo(v2)

1096 v1 = v2

1097

1098 if _MODS.booleans.isBoolean(points):

1099 if not closed:

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

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

1102 else:

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

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

1105

1106

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

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

1109

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

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

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

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

1114

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

1116

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

1118 '''

1119 try:

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

1121 except (TypeError, ValueError) as x:

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

1123

1124

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

1126 height=None, wrap=False,

1127 LatLon=LatLon, **LatLon_kwds):

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

1129 bearing from those points.

1130

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

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

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

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

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

1136 the mean height (C{meter}).

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

1138 (C{bool}).

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

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

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

1142

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

1144

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

1146

1147 @raise Valuerror: Points coincide.

1148 '''

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

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

1151 height=height, wrap=wrap,

1152 LatLon=LatLon, **LatLon_kwds)

1153

1154

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

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

1157 LatLon=LatLon, **LatLon_kwds):

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

1159

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

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

1162 as B{C{radius}}).

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

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

1165 as B{C{radius}}).

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

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

1168 as B{C{radius}}).

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

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

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

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

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

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

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

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

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

1178

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

1180

1181 @raise IntersectionError: No intersection, trilateration failed.

1182

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

1184

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

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

1187

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

1189 '''

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

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

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

1193 radius=radius, height=height, useZ=useZ,

1194 wrap=wrap, LatLon=LatLon, **LatLon_kwds)

1195

1196

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

1198 areaOf, # functions

1199 intersecant2, intersection, intersection2, ispolar,

1200 meanOf,

1201 nearestOn2, nearestOn3,

1202 perimeterOf,

1203 sumOf,

1204 triangulate, trilaterate)

1205

1206# **) MIT License

1207#

1208# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1209#

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

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

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

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

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

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

1216#

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

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

1219#

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

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

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

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

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

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

1226# OTHER DEALINGS IN THE SOFTWARE.