Coverage for pygeodesy/sphericalNvector.py: 91%

315 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-04 12:01 -0400

1 

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

3 

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

5 

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

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

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

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

10 

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

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

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

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

15U{Module latlon-nvector-spherical 

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

17 

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

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

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

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

22 

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

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

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

26 

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

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

29 

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

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

32''' 

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

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

35 

36from pygeodesy.basics import _isin, _xinstanceof, typename 

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

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

41from pygeodesy.fmath import fdot_, fmean, fsum 

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

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

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

45 _point_, _pole_ 

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

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

50 NvectorBase, _triangulate, _trilaterate 

51from pygeodesy.points import NearestOn3Tuple, notImplemented, \ 

52 ispolar # PYCHOK exported 

53from pygeodesy.props import deprecated_function, deprecated_method, \ 

54 property_RO 

55from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \ 

56 _intersecant2, LatLonSphericalBase, \ 

57 _radians2m, Datums 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1152def 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.