1

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

3

4 u'''(INTERNAL) Private, 3-D vector base class C{Vector3dBase}.

5

6 A pure Python implementation of vector-based functions by I{(C) Chris Veness

7 2011-2015} published under the same MIT Licence**, see U{Vector-based geodesy

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

9'''

10

11 from pygeodesy.basics import _copysign, islistuple, isscalar, map1, \

12 map2, _zip

13 from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, _copysignINF, \

14 _float0, isnear0, isnear1, isneg0, \

15 _pos_self, _0_0, _1_0

16 from pygeodesy.errors import CrossError, VectorError, _xcallable, _xError

17 from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_

18 from pygeodesy.interns import NN, _coincident_, _colinear_, \

19 _COMMASPACE_, _xyz_

20 from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS, \

21 _sys_version_info2

22 from pygeodesy.named import _NamedBase, _NotImplemented, _xother3

23# from pygeodesy.namedTuples import Vector3Tuple # _MODS

24 from pygeodesy.props import deprecated_method, Property, Property_RO, \

25 property_doc_, property_RO, _update_all

26 from pygeodesy.streprs import Fmt, strs, unstr

27 from pygeodesy.units import Float, Scalar

28# from pygeodesy.utily import sincos2 # _MODS

29

30 from math import atan2, ceil, fabs, floor, trunc

31

32__all__ = _ALL_LAZY.vector3dBase

33__version__ = '24.03.24'

34

35

36 class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum

37 '''(INTERNAL) Generic 3-D vector base class.

38 '''

39 _crosserrors = True # un/set by .errors.crosserrors

40

41 _ll = None # original latlon, '_fromll'

42# _x = INT0 # X component

43# _y = INT0 # Y component

44# _z = INT0 # Z component

45

46 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, name=NN):

47 '''New L{Vector3d} or C{Vector3dBase} instance.

48

49 The vector may be normalised or use x, y, z for position and

50 distance from earth centre or height relative to the surface

51 of the earth' sphere or ellipsoid.

52

53 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector

54 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},

55 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or

56 C{list} of 3+ C{scalar} items).

57 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}

58 is not C{scalar}, otherwise same units as B{C{x_xyz}}.

59 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}

60 is not C{scalar}, otherwise same units as B{C{x_xyz}}.

61 @kwarg ll: Optional latlon reference (C{LatLon}).

62 @kwarg name: Optional name (C{str}).

63

64 @raise VectorError: Invalid B{C{x_xyz}}.

65 '''

66 self._x, \

67 self._y, \

68 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \

69 _xyz3(type(self), x_xyz)

70 if ll:

71 self._ll = ll

72 if name:

73 self.name = name

74

75 def __abs__(self):

76 '''Return the norm of this vector.

77

78 @return: Norm, unit length (C{float});

79 '''

80 return self.length

81

82 def __add__(self, other):

83 '''Add this to an other vector (L{Vector3d}).

84

85 @return: Vectorial sum (L{Vector3d}).

86

87 @raise TypeError: Incompatible B{C{other}} C{type}.

88 '''

89 return self.plus(other)

90

91 def __bool__(self): # PYCHOK PyChecker

92 '''Is this vector non-zero?

93 '''

94 return bool(self.x or self.y or self.z)

95

96 def __ceil__(self): # PYCHOK no cover

97 '''Return a vector with the C{ceil} of these components.

98

99 @return: Ceil-ed (L{Vector3d}).

100 '''

101 return self._mapped(ceil)

102

103 def __cmp__(self, other): # Python 2-

104 '''Compare this and an other vector (L{Vector3d}).

105

106 @return: -1, 0 or +1 (C{int}).

107

108 @raise TypeError: Incompatible B{C{other}} C{type}.

109 '''

110 n = self.others(other).length

111 return -1 if self.length < n else (

112 +1 if self.length > n else 0)

113

114 cmp = __cmp__

115

116 def __divmod__(self, other): # PYCHOK no cover

117 '''Not implemented.'''

118 return _NotImplemented(self, other)

119

120 def __eq__(self, other):

121 '''Is this vector equal to an other vector?

122

123 @arg other: The other vector (L{Vector3d}).

124

125 @return: C{True} if equal, C{False} otherwise.

126

127 @raise TypeError: Incompatible B{C{other}} C{type}.

128 '''

129 return self.isequalTo(other, eps=EPS0)

130

131 def __float__(self): # PYCHOK no cover

132 '''Not implemented.'''

133 return _NotImplemented(self)

134

135 def __floor__(self): # PYCHOK no cover

136 '''Return a vector with the C{floor} of these components.

137

138 @return: Floor-ed (L{Vector3d}).

139 '''

140 return self._mapped(floor)

141

142 def __floordiv__(self, other): # PYCHOK no cover

143 '''Not implemented.'''

144 return _NotImplemented(self, other)

145

146 def __format__(self, *other): # PYCHOK no cover

147 '''Not implemented.'''

148 return _NotImplemented(self, *other)

149

150 def __ge__(self, other):

151 '''Is this vector longer than or equal to an other vector?

152

153 @arg other: The other vector (L{Vector3d}).

154

155 @return: C{True} if so, C{False} otherwise.

156

157 @raise TypeError: Incompatible B{C{other}} C{type}.

158 '''

159 return self.length >= self.others(other).length

160

161# def __getitem__(self, key):

162# '''Return C{item} at index or slice C{[B{key}]}.

163# '''

164# return self.xyz[key]

165

166 def __gt__(self, other):

167 '''Is this vector longer than an other vector?

168

169 @arg other: The other vector (L{Vector3d}).

170

171 @return: C{True} if so, C{False} otherwise.

172

173 @raise TypeError: Incompatible B{C{other}} C{type}.

174 '''

175 return self.length > self.others(other).length

176

177 def __hash__(self): # PYCHOK no cover

178 '''Return this instance' C{hash}.

179 '''

180 return hash(self.xyz) # XXX id(self)?

181

182 def __iadd__(self, other):

183 '''Add this and an other vector I{in-place}, C{this += B{other}}.

184

185 @arg other: The other vector (L{Vector3d}).

186

187 @raise TypeError: Incompatible B{C{other}} C{type}.

188 '''

189 return self._xyz(self.plus(other))

190

191 def __ifloordiv__(self, other): # PYCHOK no cover

192 '''Not implemented.'''

193 return _NotImplemented(self, other)

194

195 def __imatmul__(self, other): # PYCHOK Python 3.5+

196 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.

197

198 @arg other: The other vector (L{Vector3d}).

199

200 @raise TypeError: Incompatible B{C{other}} C{type}.

201

202 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.

203 '''

204 return self._xyz(self.cross(other))

205

206 def __imod__(self, other): # PYCHOK no cover

207 '''Not implemented.'''

208 return _NotImplemented(self, other)

209

210 def __imul__(self, scalar):

211 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.

212

213 @arg scalar: Factor (C{scalar}).

214

215 @raise TypeError: Non-scalar B{C{scalar}}.

216 '''

217 return self._xyz(self.times(scalar))

218

219 def __int__(self): # PYCHOK no cover

220 '''Return a vector with the C{int} of these components.

221

222 @return: Int-ed (L{Vector3d}).

223 '''

224 v = self.classof(_0_0)

225 v._x, v._y, v._z = map2(int, self.xyz)

226 return v

227

228 def __ipow__(self, other, *mod): # PYCHOK no cover

229 '''Not implemented.'''

230 return _NotImplemented(self, other, *mod)

231

232 def __isub__(self, other):

233 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.

234

235 @arg other: The other vector (L{Vector3d}).

236

237 @raise TypeError: Incompatible B{C{other}} C{type}.

238 '''

239 return self._xyz(self.minus(other))

240

241# def __iter__(self):

242# '''Return an C{iter}ator over this vector's components.

243# '''

244# return iter(self.xyz)

245

246 def __itruediv__(self, scalar):

247 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.

248

249 @arg scalar: The divisor (C{scalar}).

250

251 @raise TypeError: Non-scalar B{C{scalar}}.

252 '''

253 return self._xyz(self.dividedBy(scalar))

254

255 def __le__(self, other): # Python 3+

256 '''Is this vector shorter than or equal to an other vector?

257

258 @arg other: The other vector (L{Vector3d}).

259

260 @return: C{True} if so, C{False} otherwise.

261

262 @raise TypeError: Incompatible B{C{other}} C{type}.

263 '''

264 return self.length <= self.others(other).length

265

266# def __len__(self):

267# '''Return C{3}, always.

268# '''

269# return len(self.xyz)

270

271 def __lt__(self, other): # Python 3+

272 '''Is this vector shorter than an other vector?

273

274 @arg other: The other vector (L{Vector3d}).

275

276 @return: C{True} if so, C{False} otherwise.

277

278 @raise TypeError: Incompatible B{C{other}} C{type}.

279 '''

280 return self.length < self.others(other).length

281

282 def __matmul__(self, other): # PYCHOK Python 3.5+

283 '''Compute the cross product of this and an other vector, C{this @ B{other}}.

284

285 @arg other: The other vector (L{Vector3d}).

286

287 @return: Cross product (L{Vector3d}).

288

289 @raise TypeError: Incompatible B{C{other}} C{type}.

290 '''

291 return self.cross(other)

292

293 def __mod__(self, other): # PYCHOK no cover

294 '''Not implemented.'''

295 return _NotImplemented(self, other)

296

297 def __mul__(self, scalar):

298 '''Multiply this vector by a scalar, C{this * B{scalar}}.

299

300 @arg scalar: Factor (C{scalar}).

301

302 @return: Product (L{Vector3d}).

303 '''

304 return self.times(scalar)

305

306 def __ne__(self, other):

307 '''Is this vector not equal to an other vector?

308

309 @arg other: The other vector (L{Vector3d}).

310

311 @return: C{True} if so, C{False} otherwise.

312

313 @raise TypeError: Incompatible B{C{other}} C{type}.

314 '''

315 return not self.isequalTo(other, eps=EPS0)

316

317 def __neg__(self):

318 '''Return the opposite of this vector.

319

320 @return: This instance negated (L{Vector3d})

321 '''

322 return self.classof(-self.x, -self.y, -self.z)

323

324 def __pos__(self): # PYCHOK no cover

325 '''Return this vector I{as-is} or a copy.

326

327 @return: This instance (L{Vector3d})

328 '''

329 return self if _pos_self else self.copy()

330

331 def __pow__(self, other, *mod): # PYCHOK no cover

332 '''Not implemented.'''

333 return _NotImplemented(self, other, *mod)

334

335 __radd__ = __add__ # PYCHOK no cover

336

337 def __rdivmod__ (self, other): # PYCHOK no cover

338 '''Not implemented.'''

339 return _NotImplemented(self, other)

340

341# def __repr__(self):

342# '''Return the default C{repr(this)}.

343# '''

344# return self.toRepr()

345

346 def __rfloordiv__(self, other): # PYCHOK no cover

347 '''Not implemented.'''

348 return _NotImplemented(self, other)

349

350 def __rmatmul__(self, other): # PYCHOK Python 3.5+

351 '''Compute the cross product of an other and this vector, C{B{other} @ this}.

352

353 @arg other: The other vector (L{Vector3d}).

354

355 @return: Cross product (L{Vector3d}).

356

357 @raise TypeError: Incompatible B{C{other}} C{type}.

358 '''

359 return self.others(other).cross(self)

360

361 def __rmod__(self, other): # PYCHOK no cover

362 '''Not implemented.'''

363 return _NotImplemented(self, other)

364

365 __rmul__ = __mul__

366

367 def __round__(self, *ndigits): # PYCHOK no cover

368 '''Return a vector with these components C{rounded}.

369

370 @arg ndigits: Optional number of digits (C{int}).

371

372 @return: Rounded (L{Vector3d}).

373 '''

374 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>

375 return self.classof(*(round(_, *ndigits) for _ in self.xyz))

376

377 def __rpow__(self, other, *mod): # PYCHOK no cover

378 '''Not implemented.'''

379 return _NotImplemented(self, other, *mod)

380

381 def __rsub__(self, other): # PYCHOK no cover

382 '''Subtract this vector from an other vector, C{B{other} - this}.

383

384 @arg other: The other vector (L{Vector3d}).

385

386 @return: Difference (L{Vector3d}).

387

388 @raise TypeError: Incompatible B{C{other}} C{type}.

389 '''

390 return self.others(other).minus(self)

391

392 def __rtruediv__(self, scalar): # PYCHOK no cover

393 '''Not implemented.'''

394 return _NotImplemented(self, scalar)

395

396# def __str__(self):

397# '''Return the default C{str(self)}.

398# '''

399# return self.toStr()

400

401 def __sub__(self, other):

402 '''Subtract an other vector from this vector, C{this - B{other}}.

403

404 @arg other: The other vector (L{Vector3d}).

405

406 @return: Difference (L{Vector3d}).

407

408 @raise TypeError: Incompatible B{C{other}} C{type}.

409 '''

410 return self.minus(other)

411

412 def __truediv__(self, scalar):

413 '''Divide this vector by a scalar, C{this / B{scalar}}.

414

415 @arg scalar: The divisor (C{scalar}).

416

417 @return: Quotient (L{Vector3d}).

418

419 @raise TypeError: Non-scalar B{C{scalar}}.

420 '''

421 return self.dividedBy(scalar)

422

423 def __trunc__(self): # PYCHOK no cover

424 '''Return a vector with the C{trunc} of these components.

425

426 @return: Trunc-ed (L{Vector3d}).

427 '''

428 return self._mapped(trunc)

429

430 if _sys_version_info2 < (3, 0): # PYCHOK no cover

431 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>

432 __div__ = __truediv__

433 __idiv__ = __itruediv__

434 __long__ = __int__

435 __nonzero__ = __bool__

436 __rdiv__ = __rtruediv__

437

438 def angleTo(self, other, vSign=None, wrap=False):

439 '''Compute the angle between this and an other vector.

440

441 @arg other: The other vector (L{Vector3d}).

442 @kwarg vSign: Optional vector, if supplied (and out of the

443 plane of this and the other), angle is signed

444 positive if this->other is clockwise looking

445 along vSign or negative in opposite direction,

446 otherwise angle is unsigned.

447 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).

448

449 @return: Angle (C{radians}).

450

451 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.

452 '''

453 x = self.cross(other)

454 s = x.length

455 # use vSign as reference to set sign of s

456 if s and vSign and x.dot(vSign) < 0:

457 s = -s

458

459 a = atan2(s, self.dot(other))

460 if wrap and fabs(a) > PI:

461 a -= _copysign(PI2, a)

462 return a

463

464 def apply(self, fun2, other_x, *y_z, **fun2_kwds):

465 '''Apply a 2-argument function pairwise to the components

466 of this and an other vector.

467

468 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),

469 return a C{scalar} or L{INT0} result.

470 @arg other_x: Other X component (C{scalar}) or a vector

471 with X, Y and Z components (C{Cartesian},

472 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},

473 L{Vector3Tuple} or L{Vector4Tuple}).

474 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).

475 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.

476

477 @return: New, applied vector (L{Vector3d}).

478

479 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.

480 '''

481 _xcallable(fun2=fun2)

482 if fun2_kwds:

483 def _f2(a, b):

484 return fun2(a, b, **fun2_kwds)

485 else:

486 _f2 = fun2

487

488 xyz = _xyz3(self.apply, other_x, *y_z)

489 xyz = (_f2(a, b) for a, b in _zip(self.xyz, xyz)) # strict=True

490 return self.classof(*xyz)

491

492 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN

493 '''Compute the cross product of this and an other vector.

494

495 @arg other: The other vector (L{Vector3d}).

496 @kwarg raiser: Optional, L{CrossError} label if raised (C{str},

497 non-L{NN}).

498 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as

499 C{x}, C{y}, and C{z}.

500

501 @return: Cross product (L{Vector3d}).

502

503 @raise CrossError: Zero or near-zero cross product and both

504 B{C{raiser}} and L{pygeodesy.crosserrors} set.

505

506 @raise TypeError: Incompatible B{C{other}} C{type}.

507 '''

508 X, Y, Z = self.others(other).xyz

509 x, y, z = self.xyz

510 xyz = ((y * Z - Y * z),

511 (z * X - Z * x),

512 (x * Y - X * y))

513

514 if raiser and self.crosserrors and eps0 > 0 \

515 and max(map(fabs, xyz)) < eps0:

516 r = other._fromll or other

517 s = self._fromll or self

518 t = self.isequalTo(other, eps=eps0)

519 t = _coincident_ if t else _colinear_

520 raise CrossError(raiser, s, other=r, txt=t)

521

522 return self.classof(*xyz)

523

524 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')

525 def crosserrors(self):

526 '''Get L{CrossError} exceptions (C{bool}).

527 '''

528 return self._crosserrors

529

530 @crosserrors.setter # PYCHOK setter!

531 def crosserrors(self, raiser):

532 '''Raise or ignore L{CrossError} exceptions (C{bool}).

533 '''

534 self._crosserrors = bool(raiser)

535

536 def dividedBy(self, divisor):

537 '''Divide this vector by a scalar.

538

539 @arg divisor: The divisor (C{scalar}).

540

541 @return: New, scaled vector (L{Vector3d}).

542

543 @raise TypeError: Non-scalar B{C{divisor}}.

544

545 @raise VectorError: Invalid or zero B{C{divisor}}.

546 '''

547 d = Scalar(divisor=divisor)

548 try:

549 return self._times(_1_0 / d)

550 except (ValueError, ZeroDivisionError) as x:

551 raise VectorError(divisor=divisor, cause=x)

552

553 def dot(self, other):

554 '''Compute the dot (scalar) product of this and an other vector.

555

556 @arg other: The other vector (L{Vector3d}).

557

558 @return: Dot product (C{float}).

559

560 @raise TypeError: Incompatible B{C{other}} C{type}.

561 '''

562 return self.length2 if other is self else \

563 fdot(self.xyz, *self.others(other).xyz)

564

565 @deprecated_method

566 def equals(self, other, units=False): # PYCHOK no cover

567 '''DEPRECATED, use method C{isequalTo}.

568 '''

569 return self.isequalTo(other, units=units)

570

571 @Property_RO

572 def euclid(self):

573 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).

574

575 @see: Properties C{length} and C{length2} and function

576 L{pygeodesy.euclid_}.

577 '''

578 return Float(euclid=euclid_(self.x, self.y, self.z))

579

580 def equirectangular(self, other):

581 '''I{Approximate} the different between this and an other vector.

582

583 @arg other: Vector to subtract (C{Vector3dBase}).

584

585 @return: The lenght I{squared} of the difference (C{Float}).

586

587 @raise TypeError: Incompatible B{C{other}} C{type}.

588

589 @see: Property C{length2}.

590 '''

591 d = self.minus(other)

592 return Float(equirectangular=hypot2_(d.x, d.y, d.z))

593

594 @Property

595 def _fromll(self):

596 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.

597 '''

598 return self._ll

599

600 @_fromll.setter # PYCHOK setter!

601 def _fromll(self, ll):

602 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.

603 '''

604 self._ll = ll or None

605

606 @property_RO

607 def homogeneous(self):

608 '''Get this vector's homogeneous representation (L{Vector3d}).

609 '''

610 x, y, z = self.xyz

611 if z:

612 x = x / z # /= chokes PyChecker

613 y = y / z

614# z = _1_0

615 else:

616 if isneg0(z):

617 x = -x

618 y = -y

619 x = _copysignINF(x)

620 y = _copysignINF(y)

621# z = NAN

622 return self.classof(x, y, _1_0)

623

624 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False

625 '''Locate the vector at a given fraction between (or along) this

626 and an other vector.

627

628 @arg other: The other vector (L{Vector3d}).

629 @arg fraction: Fraction between both vectors (C{scalar},

630 0.0 for this and 1.0 for the other vector).

631

632 @return: Intermediate vector (L{Vector3d}).

633

634 @raise TypeError: Incompatible B{C{other}} C{type}.

635 '''

636 f = Scalar(fraction=fraction)

637 if isnear0(f): # PYCHOK no cover

638 r = self

639 else:

640 r = self.others(other)

641 if not isnear1(f): # self * (1 - f) + r * f

642 r = self.plus(r.minus(self)._times(f))

643 return r

644

645 def isconjugateTo(self, other, minum=1, eps=EPS):

646 '''Determine whether this and an other vector are conjugates.

647

648 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},

649 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).

650 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).

651 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),

652 same units as C{x}, C{y}, and C{z}.

653

654 @return: C{True} if both vector's components either match

655 or at least C{B{minum}} have opposite signs.

656

657 @raise TypeError: Incompatible B{C{other}} C{type}.

658

659 @see: Method C{isequalTo}.

660 '''

661 self.others(other)

662 n = 0

663 for a, b in zip(self.xyz, other.xyz):

664 if fabs(a + b) < eps and ((a < 0 and b > 0) or

665 (a > 0 and b < 0)):

666 n += 1 # conjugate

667 elif fabs(a - b) > eps:

668 return False # unequal

669 return bool(n >= minum)

670

671 def isequalTo(self, other, units=False, eps=EPS):

672 '''Check if this and an other vector are equal or equivalent.

673

674 @arg other: The other vector (L{Vector3d}).

675 @kwarg units: Optionally, compare the normalized, unit

676 version of both vectors.

677 @kwarg eps: Tolerance for equality (C{scalar}), same units as

678 C{x}, C{y}, and C{z}.

679

680 @return: C{True} if vectors are identical, C{False} otherwise.

681

682 @raise TypeError: Incompatible B{C{other}} C{type}.

683

684 @see: Method C{isconjugateTo}.

685 '''

686 if units:

687 self.others(other)

688 d = self.unit().minus(other.unit())

689 else:

690 d = self.minus(other)

691 return max(map(fabs, d.xyz)) < eps

692

693 @Property_RO

694 def length(self): # __dict__ value overwritten by Property_RO C{_united}

695 '''Get the length (norm, magnitude) of this vector (C{Float}).

696

697 @see: Properties L{length2} and L{euclid}.

698 '''

699 return Float(length=hypot_(self.x, self.y, self.z))

700

701 @Property_RO

702 def length2(self): # __dict__ value overwritten by Property_RO C{_united}

703 '''Get the length I{squared} of this vector (C{Float}).

704

705 @see: Property L{length} and method C{equirectangular}.

706 '''

707 return Float(length2=hypot2_(self.x, self.y, self.z))

708

709 def _mapped(self, func):

710 '''(INTERNAL) Map these components.

711 '''

712 return self.classof(*map2(func, self.xyz))

713

714 def minus(self, other):

715 '''Subtract an other vector from this vector.

716

717 @arg other: The other vector (L{Vector3d}).

718

719 @return: New vector difference (L{Vector3d}).

720

721 @raise TypeError: Incompatible B{C{other}} C{type}.

722 '''

723 xyz = self.others(other).xyz

724 return self._minus(*xyz)

725

726 def _minus(self, x, y, z):

727 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.

728 '''

729 return self.classof(self.x - x, self.y - y, self.z - z)

730

731 def minus_(self, other_x, *y_z):

732 '''Subtract separate X, Y and Z components from this vector.

733

734 @arg other_x: X component (C{scalar}) or a vector's

735 X, Y, and Z components (C{Cartesian},

736 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},

737 L{Vector3Tuple}, L{Vector4Tuple}).

738 @arg y_z: Y and Z components (C{scalar}, C{scalar}),

739 ignored if B{C{other_x}} is not C{scalar}.

740

741 @return: New, vectiorial vector (L{Vector3d}).

742

743 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.

744 '''

745 return self._minus(*_xyz3(self.minus_, other_x, *y_z))

746

747 def negate(self):

748 '''Return this vector in opposite direction.

749

750 @return: New, opposite vector (L{Vector3d}).

751 '''

752 return self.classof(-self.x, -self.y, -self.z)

753

754 __neg__ = negate # PYCHOK no cover

755

756 @Property_RO

757 def _N_vector(self):

758 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})

759 '''

760 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name)

761

762 def others(self, *other, **name_other_up):

763 '''Refined class comparison.

764

765 @arg other: The other vector (L{Vector3d}).

766 @kwarg name_other_up: Overriding C{name=other} and C{up=1}

767 keyword arguments.

768

769 @return: The B{C{other}} if compatible.

770

771 @raise TypeError: Incompatible B{C{other}} C{type}.

772 '''

773 other, name, up = _xother3(self, other, **name_other_up)

774 if not isinstance(other, Vector3dBase):

775 _NamedBase.others(self, other, name=name, up=up + 1)

776 return other

777

778 def plus(self, other):

779 '''Add this vector and an other vector.

780

781 @arg other: The other vector (L{Vector3d}).

782

783 @return: Vectorial sum (L{Vector3d}).

784

785 @raise TypeError: Incompatible B{C{other}} C{type}.

786 '''

787 xyz = self.others(other).xyz

788 return self._plus(*xyz)

789

790 sum = plus # alternate name

791

792 def _plus(self, x, y, z):

793 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.

794 '''

795 return self.classof(self.x + x, self.y + y, self.z + z)

796

797 def plus_(self, other_x, *y_z):

798 '''Sum of this vector and separate X, Y and Z components.

799

800 @arg other_x: X component (C{scalar}) or a vector's

801 X, Y, and Z components (C{Cartesian},

802 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},

803 L{Vector3Tuple}, L{Vector4Tuple}).

804 @arg y_z: Y and Z components (C{scalar}, C{scalar}),

805 ignored if B{C{other_x}} is not C{scalar}.

806

807 @return: New, vectiorial vector (L{Vector3d}).

808

809 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.

810 '''

811 return self._plus(*_xyz3(self.plus_, other_x, *y_z))

812

813 def rotate(self, axis, theta):

814 '''Rotate this vector around an axis by a specified angle.

815

816 @arg axis: The axis being rotated around (L{Vector3d}).

817 @arg theta: The angle of rotation (C{radians}).

818

819 @return: New, rotated vector (L{Vector3d}).

820

821 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/

822 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and

823 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/

824 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.

825 '''

826 s, c = _MODS.utily.sincos2(theta) # rotation angle

827 d = _1_0 - c

828 if d or s:

829 p = self.unit().xyz # point being rotated

830 r = self.others(axis=axis).unit() # axis being rotated around

831

832 ax, ay, az = r.xyz # quaternion-derived rotation matrix

833 bx, by, bz = r.times(d).xyz

834 sx, sy, sz = r.times(s).xyz

835

836 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)

837 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)

838 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)

839 else: # unrotated

840 x, y, z = self.xyz

841 return self.classof(x, y, z)

842

843 @deprecated_method

844 def rotateAround(self, axis, theta): # PYCHOK no cover

845 '''DEPRECATED, use method C{rotate}.'''

846 return self.rotate(axis, theta)

847

848 def times(self, factor):

849 '''Multiply this vector by a scalar.

850

851 @arg factor: Scale factor (C{scalar}).

852

853 @return: New, scaled vector (L{Vector3d}).

854

855 @raise TypeError: Non-scalar B{C{factor}}.

856 '''

857 return self._times(Scalar(factor=factor))

858

859 def _times(self, s):

860 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.

861 '''

862 return self.classof(self.x * s, self.y * s, self.z * s)

863

864 def times_(self, other_x, *y_z):

865 '''Multiply this vector's components by separate X, Y and Z factors.

866

867 @arg other_x: X scale factor (C{scalar}) or a vector's

868 X, Y, and Z components as scale factors

869 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},

870 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).

871 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),

872 ignored if B{C{other_x}} is not C{scalar}.

873

874 @return: New, scaled vector (L{Vector3d}).

875

876 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.

877 '''

878 x, y, z = _xyz3(self.times_, other_x, *y_z)

879 return self.classof(self.x * x, self.y * y, self.z * z)

880

881# @deprecated_method

882# def to2ab(self): # PYCHOK no cover

883# '''DEPRECATED, use property C{Nvector.philam}.

884#

885# @return: A L{PhiLam2Tuple}C{(phi, lam)}.

886# '''

887# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)

888

889# @deprecated_method

890# def to2ll(self): # PYCHOK no cover

891# '''DEPRECATED, use property C{Nvector.latlon}.

892#

893# @return: A L{LatLon2Tuple}C{(lat, lon)}.

894# '''

895# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)

896

897 @deprecated_method

898 def to3xyz(self): # PYCHOK no cover

899 '''DEPRECATED, use property L{xyz}.

900 '''

901 return self.xyz

902

903 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected

904 '''Return a string representation of this vector.

905

906 @kwarg prec: Number of decimal places (C{int}).

907 @kwarg fmt: Enclosing format to use (C{str}).

908 @kwarg sep: Separator between components (C{str}).

909

910 @return: Vector as "(x, y, z)" (C{str}).

911 '''

912 t = sep.join(strs(self.xyz, prec=prec))

913 return (fmt % (t,)) if fmt else t

914

915 def unit(self, ll=None):

916 '''Normalize this vector to unit length.

917

918 @kwarg ll: Optional, original location (C{LatLon}).

919

920 @return: Normalized vector (L{Vector3d}).

921 '''

922 u = self._united

923 if ll:

924 u._fromll = ll

925 return u

926

927 @Property_RO

928 def _united(self): # __dict__ value overwritten below

929 '''(INTERNAL) Get normalized vector (L{Vector3d}).

930 '''

931 n = self.length

932 if n > EPS0 and fabs(n - _1_0) > EPS0:

933 u = self._xnamed(self.dividedBy(n))

934 u._update(False, length=_1_0, length2=_1_0, _united=u)

935 else:

936 u = self.copy()

937 u._update(False, _united=u)

938 if self._fromll:

939 u._fromll = self._fromll

940 return u

941

942 @Property

943 def x(self):

944 '''Get the X component (C{float}).

945 '''

946 return self._x

947

948 @x.setter # PYCHOK setter!

949 def x(self, x):

950 '''Set the X component, if different (C{float}).

951 '''

952 x = Float(x=x)

953 if self._x != x:

954 _update_all(self, needed=3)

955 self._x = x

956

957 @Property

958 def xyz(self):

959 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).

960 '''

961 return _MODS.namedTuples.Vector3Tuple(self.x, self.y, self.z, name=self.name)

962

963 @xyz.setter # PYCHOK setter!

964 def xyz(self, xyz):

965 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},

966 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}

967 or a C{tuple} or C{list} of 3+ C{scalar} items).

968 '''

969 self._xyz(xyz)

970

971 def _xyz(self, x_xyz, *y_z):

972 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.

973 '''

974 _update_all(self, needed=3)

975 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z)

976 return self

977

978 @property_RO

979 def x2y2z2(self):

980 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).

981 '''

982 return self.x**2, self.y**2, self.z**2

983

984 @Property

985 def y(self):

986 '''Get the Y component (C{float}).

987 '''

988 return self._y

989

990 @y.setter # PYCHOK setter!

991 def y(self, y):

992 '''Set the Y component, if different (C{float}).

993 '''

994 y = Float(y=y)

995 if self._y != y:

996 _update_all(self, needed=3)

997 self._y = y

998

999 @Property

1000 def z(self):

1001 '''Get the Z component (C{float}).

1002 '''

1003 return self._z

1004

1005 @z.setter # PYCHOK setter!

1006 def z(self, z):

1007 '''Set the Z component, if different (C{float}).

1008 '''

1009 z = Float(z=z)

1010 if self._z != z:

1011 _update_all(self, needed=3)

1012 self._z = z

1013

1014

1015 def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3

1016 '''(INTERNAL) Helper for C{Vector3dBase.__init__}, C{-.apply}, C{-.times_} and C{-._xyz}.

1017 '''

1018 try:

1019 x_y_z = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for VectorXTuple

1020 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else

1021 x_xyz.xyz)

1022 except (AttributeError, TypeError, ValueError) as x:

1023 raise _xError(x, unstr(where, x_xyz, *y_z))

1024 return x_y_z

1025

1026

1027__all__ += _ALL_DOCS(Vector3dBase)

1028

1029# **) MIT License

1030#

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

1032#

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

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

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

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

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

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

1039#

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

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

1042#

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

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

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

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

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

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

1049# OTHER DEALINGS IN THE SOFTWARE.