1

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

3

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

5

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

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

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

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

10

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

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

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

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

15 U{Module latlon-nvector-spherical

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

17

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

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

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

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

22

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

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

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

26

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

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

29

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

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

32'''

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

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

35

36# from pygeodesy.basics import _xinstanceof # _MODS

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

38 _0_0, _0_5, _1_0

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

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

41 from pygeodesy.fmath import fmean, fsum

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

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

44 _point_, _pole_

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

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

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

48 from pygeodesy.nvectorBase import LatLonNvectorBase, NorthPole, \

49 notImplemented, NvectorBase, _nsumOf, \

50 _triangulate, _trilaterate

51 from pygeodesy.points import NearestOn3Tuple, ispolar # PYCHOK exported

52 from pygeodesy.props import deprecated_function, deprecated_method, \

53 property_RO

54 from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \

55 _intersecant2, LatLonSphericalBase, \

56 _radians2m, Datums

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

58 from 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__ = '23.12.03'

65

66_lines_ = 'lines'

67

68

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

110 class 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 B{C{radius}} is C{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

307 the 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}, provided

313 L{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), floats=True) # 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 B{C{radius}} is C{None} and the C{angle} from this

664 to 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 B{C{Nvector}} is C{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

717 class 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_Nvll = LatLon(_0_0, _0_0, name=_Nv00_) # reference instance (L{LatLon})

801

802

803 def 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 B{C{radius}} is C{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 = _Nvll.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), floats=True)

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

856 def 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 B{C{radius}})

862 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

867 method L{LatLon.intersecant2} for further details.

868

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

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

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

872

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

874

875 @raise TypeError: If B{C{center}} or B{C{point}} not L{LatLon} or

876 B{C{circle}} or B{C{other}} invalid.

877

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

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

880 '''

881 c = _Nvll.others(center=center)

882 p = _Nvll.others(point=point)

883 try:

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

885 except (TypeError, ValueError) as x:

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

887 **radius_exact_height_wrap)

888

889

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

891 LatLon=LatLon, **LatLon_kwds):

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

893 by two points or by a start 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 initial

897 bearing at the first start point (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 initial

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

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

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

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

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

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

906 (L{LatLon}).

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

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

909

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

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

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

913

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

915

916 @raise ValueError: Intersection is ambiguous or infinite or

917 the lines are parallel, coincident or null.

918

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

920 '''

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

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

923 return i.toLatLon(**kwds)

924

925

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

927 LatLon=LatLon, **LatLon_kwds):

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

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

930

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

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

933 initial bearing at the first start point

934 (compass C{degrees360}).

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

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

937 initial bearing at the second start point

938 (compass C{degrees360}).

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

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

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

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

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

944 antipodal points (L{LatLon}).

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

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

947

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

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

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

951 if available.

952

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

954

955 @raise ValueError: Intersection is ambiguous or infinite or

956 the lines are parallel, coincident or null.

957

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

959 '''

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

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

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

963

964

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

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

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

968 '''

969 p1 = _Nvll.others(start1=start1)

970 p2 = _Nvll.others(start2=start2)

971 if wrap:

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

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

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

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

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

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

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

979 gc1, s1, e1 = _Nvll._gc3(p1, end1, 'end1', wrap=wrap)

980 gc2, s2, e2 = _Nvll._gc3(p2, end2, 'end2', wrap=wrap)

981

982 hs = start1.height, start2.height

983 # there are two (antipodal) candidate intersection

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

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

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

987

988 # selection of intersection point depends on how

989 # lines are defined (by bearings or endpoints)

990 if e1 and e2: # endpoint+endpoint

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

992 hs += end1.height, end2.height

993 elif e1 and not e2: # endpoint+bearing

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

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

996 hs += end1.height,

997 elif e2 and not e1: # bearing+endpoint

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

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

1000 hs += end2.height,

1001 else: # bearing+bearing

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

1003 # towards i1, otherwise towards antipodal i2

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

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

1006 if d1 > 0 and d2 > 0:

1007 d = 1 # both point to i1

1008 elif d1 < 0 and d2 < 0:

1009 d = -1 # both point to i2

1010 else: # d1, d2 opposite signs

1011 # intersection is at further-away intersection point,

1012 # take opposite intersection from mid- point of v1

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

1014 # get intersection p1 bearing points to, aka being

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

1016 # function .sphericalTrigonometry._intersect and

1017 # .ellipsoidalBaseDI._intersect3

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

1019

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

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

1022

1023

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

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

1026

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

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

1029 (C{meter}).

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

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

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

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

1034

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

1036

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

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

1039 '''

1040 def _N_vs(ps, w):

1041 Ps = _Nvll.PointsIter(ps, wrap=w)

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

1043 yield p._N_vector

1044

1045 try:

1046 # geographic mean

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

1048 except (TypeError, ValueError) as x:

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

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

1051 name=meanOf.__name__))

1052

1053

1054@deprecated_function

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

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

1057

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

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

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

1061 '''

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

1063 return r.closest, r.distance

1064

1065

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

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

1068 consecutive points) closest to an other point.

1069

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

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

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

1073

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

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

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

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

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

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

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

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

1082

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

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

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

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

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

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

1089 C{degrees360}.

1090

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

1092

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

1094 '''

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

1096

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

1098 height=height, wrap=wrap)

1099

1100

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

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

1103 great circle arcs joining consecutive points).

1104

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

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

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

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

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

1110

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

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

1113

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

1115

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

1117

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

1119 C{B{points}} a composite.

1120

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

1122 and L{sphericalTrigonometry.perimeterOf}.

1123 '''

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

1125 Ps = _Nvll.PointsIter(ps, loop=1, wrap=w)

1126 p1 = Ps[0]

1127 v1 = p1._N_vector

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

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

1130 p2 = _unrollon(p1, p2)

1131 p1 = p2

1132 v2 = p2._N_vector

1133 yield v1.angleTo(v2)

1134 v1 = v2

1135

1136 if _MODS.booleans.isBoolean(points):

1137 if not closed:

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

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

1140 else:

1141 r = fsum(_rads(points, closed, wrap), floats=True)

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

1143

1144

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

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

1147

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

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

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

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

1152

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

1154

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

1156 '''

1157 try:

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

1159 except (TypeError, ValueError) as x:

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

1161

1162

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

1164 height=None, wrap=False,

1165 LatLon=LatLon, **LatLon_kwds):

1166 '''Locate a point given two known points and the initial bearings

1167 from those points.

1168

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

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

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

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

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

1174 the mean height (C{meter}).

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

1176 (C{bool}).

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

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

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

1180

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

1182

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

1184

1185 @raise Valuerror: Points coincide.

1186 '''

1187 return _triangulate(_Nvll.others(point1=point1), bearing1,

1188 _Nvll.others(point2=point2), bearing2,

1189 height=height, wrap=wrap,

1190 LatLon=LatLon, **LatLon_kwds)

1191

1192

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

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

1195 LatLon=LatLon, **LatLon_kwds):

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

1197

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

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

1200 as B{C{radius}}).

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

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

1203 as B{C{radius}}).

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

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

1206 as B{C{radius}}).

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

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

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

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

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

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

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

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

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

1216

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

1218

1219 @raise IntersectionError: No intersection, trilateration failed.

1220

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

1222

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

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

1225

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

1227 '''

1228 return _trilaterate(_Nvll.others(point1=point1), distance1,

1229 _Nvll.others(point2=point2), distance2,

1230 _Nvll.others(point3=point3), distance3,

1231 radius=radius, height=height, useZ=useZ,

1232 wrap=wrap, LatLon=LatLon, **LatLon_kwds)

1233

1234

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

1236 areaOf, # functions

1237 intersecant2, intersection, intersection2, ispolar,

1238 meanOf,

1239 nearestOn2, nearestOn3,

1240 perimeterOf,

1241 sumOf,

1242 triangulate, trilaterate)

1243

1244# **) MIT License

1245#

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

1247#

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

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

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

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

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

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

1254#

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

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

1257#

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

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

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

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

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

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

1264# OTHER DEALINGS IN THE SOFTWARE.