Coverage for pygeodesy/sphericalNvector.py: 91%

316 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-22 18:16 -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 

36# from pygeodesy.basics import _xinstanceof # _MODS 

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 fmean, fsum 

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

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

44 _point_, _pole_ 

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

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

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

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

49 NvectorBase, _triangulate, _trilaterate 

50from pygeodesy.points import NearestOn3Tuple, notImplemented, \ 

51 ispolar # PYCHOK exported 

52from pygeodesy.props import deprecated_function, deprecated_method, \ 

53 property_RO 

54from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \ 

55 _intersecant2, LatLonSphericalBase, \ 

56 _radians2m, Datums 

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

58from pygeodesy.utily import atan2, degrees360, fabs, sincos2, sincos2_, \ 

59 sincos2d, _unrollon, _Wrap 

60 

61# from math import atan2, fabs # from utily 

62 

63__all__ = _ALL_LAZY.sphericalNvector 

64__version__ = '24.10.19' 

65 

66_lines_ = 'lines' 

67 

68 

69class Cartesian(CartesianSphericalBase): 

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

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

72 ''' 

73 

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

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

76 

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

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

79 to override this L{LatLon} class or specify 

80 C{B{LatLon}=None}. 

81 

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

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

84 

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

86 ''' 

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

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

89 

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

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

92 

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

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

95 to override this C{Nvector} class or specify 

96 C{B{Nvector}=None}. 

97 

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

99 

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

101 ''' 

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

103 # datum=datum or self.datum) 

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

105 # return ll.toNvector(**kwds) 

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

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

108 

109 

110class LatLon(LatLonNvectorBase, LatLonSphericalBase): 

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

112 

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

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

115 ''' 

116 _Nv = None # cached_toNvector C{Nvector}) 

117 

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

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

120 ''' 

121 if updated: # reset caches 

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

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

124 

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

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

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

128 end point. 

129 

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

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

132 from the start point to the point where the perpendicular 

133 crosses the line. 

134 

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

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

137 initial bearing from start point (compass 

138 C{degrees360}). 

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

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

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

142 

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

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

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

146 toward the end point of the line or negative if 

147 "before" the start point. 

148 

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

150 

151 @raise Valuerror: Some points coincide. 

152 ''' 

153 p = self.others(start=start) 

154 n = self.toNvector() 

155 

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

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

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

159 

160 @deprecated_method 

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

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

163 ''' 

164 return self.initialBearingTo(other) 

165 

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

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

168 defined by a start and end point. 

169 

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

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

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

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

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

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

176 

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

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

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

180 of the line . 

181 

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

183 

184 @raise Valuerror: Some points coincide. 

185 ''' 

186 p = self.others(start=start) 

187 n = self.toNvector() 

188 

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

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

191 

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

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

194 the given distance on the given bearing. 

195 

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

197 B{C{radius}}). 

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

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

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

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

202 

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

204 

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

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

207 ''' 

208 b = Bearing_(bearing) 

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

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

211 

212 n = self.toNvector() 

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

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

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

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

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

218 

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

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

221 

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

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

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

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

226 

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

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

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

230 

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

232 ''' 

233 p = self.others(other) 

234 if wrap: 

235 p = _unrollon(self, p) 

236 n = p.toNvector() 

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

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

239 

240# @Property_RO 

241# def Ecef(self): 

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

243# ''' 

244# return _ALL_MODS.ecef.EcefKarney 

245 

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

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

248 ''' 

249 s = start.toNvector() 

250 if _isDegrees(end): # bearing 

251 gc = s.greatCircle(end) 

252 e = None 

253 else: # point 

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

255 if wrap: 

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

257 e = p.toNvector() 

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

259 return gc, s, e 

260 

261 def greatCircle(self, bearing): 

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

263 heading on the given bearing from this point. 

264 

265 Direction of vector is such that initial bearing vector 

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

267 

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

269 

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

271 ''' 

272 t = Bearing_(bearing) 

273 a, b = self.philam 

274 

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

276 return Nvector(sb * ct - sa * cb * st, 

277 -cb * ct - sa * sb * st, 

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

279 

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

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

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

283 

284 Direction of vector is such that initial bearing vector 

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

286 

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

288 this point (compass C{degrees360}). 

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

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

291 

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

293 

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

295 

296 @raise Valuerror: Points coincide. 

297 ''' 

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

299 return gc.unit() 

300 

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

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

303 to an other point. 

304 

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

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

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

308 

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

310 

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

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

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

314 

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

316 ''' 

317 n = self.toNvector() 

318 p = self.others(other) 

319 if wrap: 

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

321 p = p.toNvector() 

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

323# gc1 = self.greatCircleTo(other) 

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

325# gc2 = self.greatCircleTo(NorthPole) 

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

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

328 

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

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

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

332 

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

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

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

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

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

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

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

340 

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

342 

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

344 ''' 

345 n = self.toNvector() 

346 p = self.others(other) 

347 if wrap: 

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

349 

350 f = Scalar(fraction=fraction) 

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

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

353 

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

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

356 

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

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

359 other point. 

360 

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

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

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

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

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

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

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

368 

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

370 

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

372 

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

374 

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

376 ''' 

377 n = self.toNvector() 

378 p = self.others(other) 

379 if wrap: 

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

381 p = p.toNvector() 

382 f = Scalar(fraction=fraction) 

383 

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

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

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

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

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

389 

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

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

392 

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

394 '''Locate the intersection point of two lines each defined 

395 by two points or a start point and bearing from North. 

396 

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

398 initial bearing at this point (compass C{degrees360}). 

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

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

401 initial bearing at the second point (compass 

402 C{degrees}). 

403 @kwarg height: Optional height at the intersection point, 

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

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

406 start and end points (C{bool}). 

407 

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

409 

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

411 point is not L{LatLon}. 

412 

413 @raise ValueError: Intersection is ambiguous or infinite or 

414 the lines are parallel, coincident or null. 

415 

416 @see: Function L{sphericalNvector.intersection} and method 

417 L{intersection2}. 

418 ''' 

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

420 wrap=wrap, LatLon=self.classof) 

421 

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

423 '''Locate the intersections of two (great circle) lines each defined 

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

425 

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

427 initial bearing at this point (compass C{degrees360}). 

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

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

430 initial bearing at the second start point (compass 

431 C{degrees360}). 

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

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

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

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

436 

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

438 

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

440 point is not L{LatLon}. 

441 

442 @raise ValueError: Intersection is ambiguous or infinite or 

443 the lines are parallel, coincident or null. 

444 

445 @see: Function L{sphericalNvector.intersection2} and method 

446 L{intersection}. 

447 ''' 

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

449 wrap=wrap, LatLon=self.classof) 

450 

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

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

453 

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

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

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

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

458 

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

460 C{False} otherwise. 

461 

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

463 

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

465 

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

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

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

469 ''' 

470 if _MODS.booleans.isBoolean(points): 

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

472 

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

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

475 def _subtangles(ps, w): 

476 Ps = self.PointsIter(ps, loop=1, wrap=w) 

477 n0 = self.toNvector() 

478 _m0 = n0.minus 

479 p1 = Ps[0] 

480 vs1 = _m0(p1.toNvector()) 

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

482 if w and not Ps.looped: 

483 p2 = _unrollon(p1, p2) 

484 p1 = p2 

485 vs2 = _m0(p2.toNvector()) 

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

487 vs1 = vs2 

488 

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

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

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

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

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

494 s = fsum(_subtangles(points, wrap)) # normal vector 

495 # XXX are winding number optimisations equally applicable to 

496 # spherical surface? 

497 return fabs(s) > PI 

498 

499 @deprecated_method 

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

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

502 return self.isenclosedBy(points) 

503 

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

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

506 

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

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

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

510 the same hemispere). 

511 

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

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

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

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

516 

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

518 arc, C{False} otherwise. 

519 

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

521 L{LatLon}. 

522 ''' 

523 p1 = self.others(point1=point1) 

524 p2 = self.others(point2=point2) 

525 if wrap: 

526 p1 = _Wrap.point(p1) 

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

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

529 

530 # corner case, null arc 

531 if n1.isequalTo(n2): 

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

533 

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

535 return False # PYCHOK returns 

536 

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

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

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

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

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

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

543 

544 @deprecated_method 

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

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

547 return self.iswithin(point1, point2) 

548 

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

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

551 

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

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

554 the mean height (C{meter}). 

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

556 may be negative or greater than 1.0. 

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

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

559 

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

561 

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

563 

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

565 ''' 

566 if fraction is _0_5: 

567 p = self.others(other) 

568 if wrap: 

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

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

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

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

573 else: 

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

575 return r 

576 

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

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

579 closest to this point. 

580 

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

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

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

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

585 to interpolate the height. 

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

587 given points, otherwise the closest point 

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

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

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

591 

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

593 

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

595 not supported. 

596 

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

598 ''' 

599 p1 = self.others(point1=point1) 

600 p2 = self.others(point2=point2) 

601 if wrap: 

602 p1 = _Wrap.point(p1) 

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

604 p0 = self 

605 

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

607 # closer to arc than to its endpoints, 

608 # find the closest point on the arc 

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

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

611 n = gc1.cross(gc2) 

612 

613 elif within: # for backward compatibility, XXX unwrapped 

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

615 self.distanceTo(point2)) else point2 

616 

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

618 n1 = p1.toNvector() 

619 n2 = p2.toNvector() 

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

621 if n is n1: 

622 return p1 # is point1 

623 elif n is n2: 

624 return p2 # is point2 if not wrap 

625 

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

627 if height in (None, False): # interpolate height within extent 

628 d = p1.distanceTo(p2) 

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

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

631 return p 

632 

633 # @deprecated_method 

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

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

636 

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

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

639 to that point from this point ... 

640 ''' 

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

642 return r.closest, r.distance 

643 

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

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

646 arcs joining consecutive points) closest to this point. 

647 

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

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

650 

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

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

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

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

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

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

657 the B{C{points}} (C{bool}). 

658 

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

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

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

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

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

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

665 

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

667 

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

669 ''' 

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

671 _r = self.distanceTo 

672 _n = self.nearestOn 

673 

674 c = p1 = Ps[0] 

675 r = _r(c, radius=None) # radians 

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

677 if wrap and not Ps.looped: 

678 p2 = _unrollon(p1, p2) 

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

680 d = _r(p, radius=None) # radians 

681 if d < r: 

682 c, r = p, d 

683 p1 = p2 

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

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

686 

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

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

689 

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

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

692 to override this L{Cartesian} class or specify 

693 C{B{Cartesian}=None}. 

694 

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

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

697 

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

699 ''' 

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

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

702 

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

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

705 

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

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

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

709 

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

711 

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

713 ''' 

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

715 

716 

717class Nvector(NvectorBase): 

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

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

720 n-vectors have no singularities or discontinuities. 

721 

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

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

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

725 

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

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

728 

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

730 ''' 

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

732 

733 @property_RO 

734 def sphericalNvector(self): 

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

736 ''' 

737 return type(self) 

738 

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

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

741 (ECEF) coordinates. 

742 

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

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

745 to override this L{Cartesian} class or specify 

746 C{B{Cartesian}=None}. 

747 

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

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

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

751 

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

753 ''' 

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

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

756 

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

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

759 

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

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

762 to override this L{LatLon} class or specify 

763 C{B{LatLon}=None}. 

764 

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

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

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

768 

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

770 

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

772 ''' 

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

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

775 

776 def greatCircle(self, bearing): 

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

778 heading on given compass bearing from this point as its 

779 n-vector. 

780 

781 Direction of vector is such that initial bearing vector 

782 b = c × p. 

783 

784 @arg bearing: Initial compass bearing (C{degrees}). 

785 

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

787 

788 @raise Valuerror: Polar coincidence. 

789 ''' 

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

791 

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

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

794 

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

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

797 return n.minus(e) 

798 

799 

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

801 

802 

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

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

805 great circle arcs joining consecutive points). 

806 

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

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

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

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

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

812 

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

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

815 

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

817 

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

819 

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

821 and L{ellipsoidalKarney.areaOf}. 

822 ''' 

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

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

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

826 n0 = Ps[0].toNvector() 

827 

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

829 p1 = Ps[1] 

830 v1 = p1._N_vector 

831 gc = v2.cross(v1) 

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

833 if w and not Ps.looped: 

834 p2 = _unrollon(p1, p2) 

835 p1 = p2 

836 v2 = p2._N_vector 

837 gc1 = v1.cross(v2) 

838 v1 = v2 

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

840 gc = gc1 

841 

842 if _MODS.booleans.isBoolean(points): 

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

844 else: 

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

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

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

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

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

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

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

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

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

854 

855 

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

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

858 two points or as a point and bearing. 

859 

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

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

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

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

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

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

866 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method 

867 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase. 

868 intersecant2>} for further details. 

869 

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

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

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

873 

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

875 

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

877 not L{LatLon}. 

878 

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

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

881 ''' 

882 c = _Nv00.others(center=center) 

883 p = _Nv00.others(point=point) 

884 try: 

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

886 except (TypeError, ValueError) as x: 

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

888 **radius_exact_height_wrap) 

889 

890 

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

892 LatLon=LatLon, **LatLon_kwds): 

893 '''Locate the intersections of two (great circle) lines each defined 

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

895 

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

897 @arg end1: End point of the first line (L{LatLon}) or the initial 

898 bearing at the first start point (compass C{degrees360}). 

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

900 @arg end2: End point of the second line (L{LatLon}) or the initial 

901 bearing at the second start point (compass C{degrees360}). 

902 @kwarg height: Optional height at the intersection point, 

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

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

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

906 @kwarg LatLon: Optional class to return the intersection point 

907 (L{LatLon}). 

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

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

910 

911 @return: The intersection point (B{C{LatLon}}) or if C{B{LatLon} 

912 is None}, a cartesian L{Ecef9Tuple}C{(x, y, z, lat, lon, 

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

914 

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

916 

917 @raise ValueError: Intersection is ambiguous or infinite or 

918 the lines are parallel, coincident or null. 

919 

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

921 ''' 

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

923 kwds = _xkwds(LatLon_kwds, height=h, LatLon=LatLon) 

924 return i.toLatLon(**kwds) 

925 

926 

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

928 LatLon=LatLon, **LatLon_kwds): 

929 '''Locate the intersections of two (great circle) lines each defined 

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

931 

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

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

934 initial bearing at the first start point 

935 (compass C{degrees360}). 

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

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

938 initial bearing at the second start point 

939 (compass C{degrees360}). 

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

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

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

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

944 @kwarg LatLon: Optional class to return the intersection and 

945 antipodal points (L{LatLon}). 

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

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

948 

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

950 or if C{B{LatLon} is None}, a cartesian L{Ecef9Tuple}C{(x, 

951 y, z, lat, lon, height, C, M, datum)} with C{C} and C{M} 

952 if available. 

953 

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

955 

956 @raise ValueError: Intersection is ambiguous or infinite or 

957 the lines are parallel, coincident or null. 

958 

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

960 ''' 

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

962 kwds = _xkwds(LatLon_kwds, height=h, LatLon=LatLon) 

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

964 

965 

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

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

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

969 ''' 

970 p1 = _Nv00.others(start1=start1) 

971 p2 = _Nv00.others(start2=start2) 

972 if wrap: 

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

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

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

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

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

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

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

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

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

982 

983 hs = start1.height, start2.height 

984 # there are two (antipodal) candidate intersection 

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

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

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

988 

989 # selection of intersection point depends on how 

990 # lines are defined (by bearings or endpoints) 

991 if e1 and e2: # endpoint+endpoint 

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

993 hs += end1.height, end2.height 

994 elif e1 and not e2: # endpoint+bearing 

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

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

997 hs += end1.height, 

998 elif e2 and not e1: # bearing+endpoint 

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

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

1001 hs += end2.height, 

1002 else: # bearing+bearing 

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

1004 # towards i1, otherwise towards antipodal i2 

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

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

1007 if d1 > 0 and d2 > 0: 

1008 d = 1 # both point to i1 

1009 elif d1 < 0 and d2 < 0: 

1010 d = -1 # both point to i2 

1011 else: # d1, d2 opposite signs 

1012 # intersection is at further-away intersection point, 

1013 # take opposite intersection from mid- point of v1 

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

1015 # get intersection p1 bearing points to, aka being 

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

1017 # function .sphericalTrigonometry._intersect and 

1018 # .ellipsoidalBaseDI._intersect3 

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

1020 

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

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

1023 

1024 

1025def meanOf(points, height=None, wrap=False, LatLon=LatLon, **LatLon_kwds): 

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

1027 

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

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

1030 (C{meter}). 

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

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

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

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

1035 

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

1037 

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

1039 some B{C{points}} are not C{LatLon}. 

1040 ''' 

1041 def _N_vs(ps, w): 

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

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

1044 yield p._N_vector 

1045 

1046 try: 

1047 # geographic mean 

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

1049 except (TypeError, ValueError) as x: 

1050 raise PointsError(points=points, wrap=wrap, LatLon=LatLon, cause=x) 

1051 return n.toLatLon(**_xkwds(LatLon_kwds, LatLon=LatLon, height=n.h, 

1052 name=meanOf.__name__)) 

1053 

1054 

1055@deprecated_function 

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

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

1058 

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

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

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

1062 ''' 

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

1064 return r.closest, r.distance 

1065 

1066 

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

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

1069 consecutive points) closest to an other point. 

1070 

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

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

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

1074 

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

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

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

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

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

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

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

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

1083 

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

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

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

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

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

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

1090 C{degrees360}. 

1091 

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

1093 

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

1095 ''' 

1096 _MODS.basics._xinstanceof(LatLon, point=point) 

1097 

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

1099 height=height, wrap=wrap) 

1100 

1101 

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

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

1104 great circle arcs joining consecutive points). 

1105 

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

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

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

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

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

1111 

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

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

1114 

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

1116 

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

1118 

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

1120 C{B{points}} a composite. 

1121 

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

1123 and L{sphericalTrigonometry.perimeterOf}. 

1124 ''' 

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

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

1127 p1 = Ps[0] 

1128 v1 = p1._N_vector 

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

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

1131 p2 = _unrollon(p1, p2) 

1132 p1 = p2 

1133 v2 = p2._N_vector 

1134 yield v1.angleTo(v2) 

1135 v1 = v2 

1136 

1137 if _MODS.booleans.isBoolean(points): 

1138 if not closed: 

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

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

1141 else: 

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

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

1144 

1145 

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

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

1148 

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

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

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

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

1153 

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

1155 

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

1157 ''' 

1158 try: 

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

1160 except (TypeError, ValueError) as x: 

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

1162 

1163 

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

1165 height=None, wrap=False, 

1166 LatLon=LatLon, **LatLon_kwds): 

1167 '''Locate a point given two known points and the (initial) bearing 

1168 from those points. 

1169 

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

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

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

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

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

1175 the mean height (C{meter}). 

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

1177 (C{bool}). 

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

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

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

1181 

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

1183 

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

1185 

1186 @raise Valuerror: Points coincide. 

1187 ''' 

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

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

1190 height=height, wrap=wrap, 

1191 LatLon=LatLon, **LatLon_kwds) 

1192 

1193 

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

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

1196 LatLon=LatLon, **LatLon_kwds): 

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

1198 

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

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

1201 as B{C{radius}}). 

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

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

1204 as B{C{radius}}). 

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

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

1207 as B{C{radius}}). 

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

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

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

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

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

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

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

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

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

1217 

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

1219 

1220 @raise IntersectionError: No intersection, trilateration failed. 

1221 

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

1223 

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

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

1226 

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

1228 ''' 

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

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

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

1232 radius=radius, height=height, useZ=useZ, 

1233 wrap=wrap, LatLon=LatLon, **LatLon_kwds) 

1234 

1235 

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

1237 areaOf, # functions 

1238 intersecant2, intersection, intersection2, ispolar, 

1239 meanOf, 

1240 nearestOn2, nearestOn3, 

1241 perimeterOf, 

1242 sumOf, 

1243 triangulate, trilaterate) 

1244 

1245# **) MIT License 

1246# 

1247# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

1248# 

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

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

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

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

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

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

1255# 

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

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

1258# 

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

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

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

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

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

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

1265# OTHER DEALINGS IN THE SOFTWARE.