Coverage for pygeodesy/vector3dBase.py: 93%
282 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-11-12 13:23 -0500
« prev ^ index » next coverage.py v7.2.2, created at 2023-11-12 13:23 -0500
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private, 3-D vector base class C{Vector3dBase}.
6A pure Python implementation of vector-based functions by I{(C) Chris Veness
72011-2015} published under the same MIT Licence**, see U{Vector-based geodesy
8<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.
9'''
11from pygeodesy.basics import _copysign, islistuple, isscalar, map1, _zip
12from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, _copysignINF, \
13 _float0, isnear0, isnear1, isneg0, \
14 _pos_self, _1_0
15from pygeodesy.errors import CrossError, _InvalidError, _IsnotError, \
16 VectorError
17from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_
18from pygeodesy.interns import NN, _coincident_, _colinear_, \
19 _COMMASPACE_, _y_, _z_
20from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS, \
21 _sys_version_info2
22from pygeodesy.named import _NamedBase, _NotImplemented, _xother3
23# from pygeodesy.namedTuples import Vector3Tuple # _MODS
24from pygeodesy.props import deprecated_method, Property, Property_RO, \
25 property_doc_, property_RO, _update_all
26from pygeodesy.streprs import Fmt, strs
27from pygeodesy.units import Float, Scalar
28from pygeodesy.utily import sincos2, atan2, fabs
30# from math import atan2, fabs # from .utily
32__all__ = _ALL_LAZY.vector3dBase
33__version__ = '23.10.15'
36class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
37 '''(INTERNAL) Generic 3-D vector base class.
38 '''
39 _crosserrors = True # un/set by .errors.crosserrors
41 _ll = None # original latlon, '_fromll'
42 _x = INT0 # X component
43 _y = INT0 # Y component
44 _z = INT0 # Z component
46 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, name=NN):
47 '''New L{Vector3d} or C{Vector3dBase} instance.
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.
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} values).
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}).
64 @raise VectorError: Invalid B{C{x_xyz}}.
65 '''
66 if isscalar(x_xyz):
67 self._xyz(x_xyz, y, z)
68 else:
69 self.xyz = x_xyz
70 if ll:
71 self._ll = ll
72 if name:
73 self.name = name
75 def __abs__(self):
76 '''Return the norm of this vector.
78 @return: Norm, unit length (C{float});
79 '''
80 return self.length
82 def __add__(self, other):
83 '''Add this to an other vector (L{Vector3d}).
85 @return: Vectorial sum (L{Vector3d}).
87 @raise TypeError: Incompatible B{C{other}} C{type}.
88 '''
89 return self.plus(other)
91 def __bool__(self): # PYCHOK PyChecker
92 '''Is this vector non-zero?
93 '''
94 return bool(self.x or self.y or self.z)
96 def __ceil__(self): # PYCHOK no cover
97 '''Not implemented.'''
98 return _NotImplemented(self)
100 def __cmp__(self, other): # Python 2-
101 '''Compare this and an other vector (L{Vector3d}).
103 @return: -1, 0 or +1 (C{int}).
105 @raise TypeError: Incompatible B{C{other}} C{type}.
106 '''
107 n = self.others(other).length
108 return -1 if self.length < n else (
109 +1 if self.length > n else 0)
111 cmp = __cmp__
113 def __divmod__(self, other): # PYCHOK no cover
114 '''Not implemented.'''
115 return _NotImplemented(self, other)
117 def __eq__(self, other):
118 '''Is this vector equal to an other vector?
120 @arg other: The other vector (L{Vector3d}).
122 @return: C{True} if equal, C{False} otherwise.
124 @raise TypeError: Incompatible B{C{other}} C{type}.
125 '''
126 return self.isequalTo(other, eps=EPS0)
128 def __float__(self): # PYCHOK no cover
129 '''Not implemented.'''
130 return _NotImplemented(self)
132 def __floor__(self): # PYCHOK no cover
133 '''Not implemented.'''
134 return _NotImplemented(self)
136 def __floordiv__(self, other): # PYCHOK no cover
137 '''Not implemented.'''
138 return _NotImplemented(self, other)
140 def __format__(self, *other): # PYCHOK no cover
141 '''Not implemented.'''
142 return _NotImplemented(self, *other)
144 def __ge__(self, other):
145 '''Is this vector longer than or equal to an other vector?
147 @arg other: The other vector (L{Vector3d}).
149 @return: C{True} if so, C{False} otherwise.
151 @raise TypeError: Incompatible B{C{other}} C{type}.
152 '''
153 return self.length >= self.others(other).length
155# def __getitem__(self, key):
156# '''Return C{item} at index or slice C{[B{key}]}.
157# '''
158# return self.xyz[key]
160 def __gt__(self, other):
161 '''Is this vector longer than an other vector?
163 @arg other: The other vector (L{Vector3d}).
165 @return: C{True} if so, C{False} otherwise.
167 @raise TypeError: Incompatible B{C{other}} C{type}.
168 '''
169 return self.length > self.others(other).length
171 def __hash__(self): # PYCHOK no cover
172 '''Return this instance' C{hash}.
173 '''
174 return hash(self.xyz) # XXX id(self)?
176 def __iadd__(self, other):
177 '''Add this and an other vector I{in-place}, C{this += B{other}}.
179 @arg other: The other vector (L{Vector3d}).
181 @raise TypeError: Incompatible B{C{other}} C{type}.
182 '''
183 return self._xyz(self.plus(other))
185 def __ifloordiv__(self, other): # PYCHOK no cover
186 '''Not implemented.'''
187 return _NotImplemented(self, other)
189 def __imatmul__(self, other): # PYCHOK Python 3.5+
190 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
192 @arg other: The other vector (L{Vector3d}).
194 @raise TypeError: Incompatible B{C{other}} C{type}.
196 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
197 '''
198 return self._xyz(self.cross(other))
200 def __imod__(self, other): # PYCHOK no cover
201 '''Not implemented.'''
202 return _NotImplemented(self, other)
204 def __imul__(self, scalar):
205 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
207 @arg scalar: Factor (C{scalar}).
209 @raise TypeError: Non-scalar B{C{scalar}}.
210 '''
211 return self._xyz(self.times(scalar))
213 def __int__(self): # PYCHOK no cover
214 '''Not implemented.'''
215 return _NotImplemented(self)
217 def __ipow__(self, other, *mod): # PYCHOK no cover
218 '''Not implemented.'''
219 return _NotImplemented(self, other, *mod)
221 def __isub__(self, other):
222 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
224 @arg other: The other vector (L{Vector3d}).
226 @raise TypeError: Incompatible B{C{other}} C{type}.
227 '''
228 return self._xyz(self.minus(other))
230# def __iter__(self):
231# '''Return an C{iter}ator over this vector's components.
232# '''
233# return iter(self.xyz)
235 def __itruediv__(self, scalar):
236 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
238 @arg scalar: The divisor (C{scalar}).
240 @raise TypeError: Non-scalar B{C{scalar}}.
241 '''
242 return self._xyz(self.dividedBy(scalar))
244 def __le__(self, other): # Python 3+
245 '''Is this vector shorter than or equal to an other vector?
247 @arg other: The other vector (L{Vector3d}).
249 @return: C{True} if so, C{False} otherwise.
251 @raise TypeError: Incompatible B{C{other}} C{type}.
252 '''
253 return self.length <= self.others(other).length
255# def __len__(self):
256# '''Return C{3}, always.
257# '''
258# return len(self.xyz)
260 def __lt__(self, other): # Python 3+
261 '''Is this vector shorter than an other vector?
263 @arg other: The other vector (L{Vector3d}).
265 @return: C{True} if so, C{False} otherwise.
267 @raise TypeError: Incompatible B{C{other}} C{type}.
268 '''
269 return self.length < self.others(other).length
271 def __matmul__(self, other): # PYCHOK Python 3.5+
272 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
274 @arg other: The other vector (L{Vector3d}).
276 @return: Cross product (L{Vector3d}).
278 @raise TypeError: Incompatible B{C{other}} C{type}.
279 '''
280 return self.cross(other)
282 def __mod__(self, other): # PYCHOK no cover
283 '''Not implemented.'''
284 return _NotImplemented(self, other)
286 def __mul__(self, scalar):
287 '''Multiply this vector by a scalar, C{this * B{scalar}}.
289 @arg scalar: Factor (C{scalar}).
291 @return: Product (L{Vector3d}).
292 '''
293 return self.times(scalar)
295 def __ne__(self, other):
296 '''Is this vector not equal to an other vector?
298 @arg other: The other vector (L{Vector3d}).
300 @return: C{True} if so, C{False} otherwise.
302 @raise TypeError: Incompatible B{C{other}} C{type}.
303 '''
304 return not self.isequalTo(other, eps=EPS0)
306 def __neg__(self):
307 '''Return the opposite of this vector.
309 @return: This instance negated (L{Vector3d})
310 '''
311 return self.classof(-self.x, -self.y, -self.z)
313 def __pos__(self): # PYCHOK no cover
314 '''Return this vector I{as-is} or a copy.
316 @return: This instance (L{Vector3d})
317 '''
318 return self if _pos_self else self.copy()
320 def __pow__(self, other, *mod): # PYCHOK no cover
321 '''Not implemented.'''
322 return _NotImplemented(self, other, *mod)
324 __radd__ = __add__ # PYCHOK no cover
326 def __rdivmod__ (self, other): # PYCHOK no cover
327 '''Not implemented.'''
328 return _NotImplemented(self, other)
330# def __repr__(self):
331# '''Return the default C{repr(this)}.
332# '''
333# return self.toRepr()
335 def __rfloordiv__(self, other): # PYCHOK no cover
336 '''Not implemented.'''
337 return _NotImplemented(self, other)
339 def __rmatmul__(self, other): # PYCHOK Python 3.5+
340 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
342 @arg other: The other vector (L{Vector3d}).
344 @return: Cross product (L{Vector3d}).
346 @raise TypeError: Incompatible B{C{other}} C{type}.
347 '''
348 return self.others(other).cross(self)
350 def __rmod__(self, other): # PYCHOK no cover
351 '''Not implemented.'''
352 return _NotImplemented(self, other)
354 __rmul__ = __mul__
356 def __round__(self, ndigits=None): # PYCHOK no cover
357 '''Not implemented.'''
358 return _NotImplemented(self, ndigits=ndigits)
360 def __rpow__(self, other, *mod): # PYCHOK no cover
361 '''Not implemented.'''
362 return _NotImplemented(self, other, *mod)
364 def __rsub__(self, other): # PYCHOK no cover
365 '''Subtract this vector from an other vector, C{B{other} - this}.
367 @arg other: The other vector (L{Vector3d}).
369 @return: Difference (L{Vector3d}).
371 @raise TypeError: Incompatible B{C{other}} C{type}.
372 '''
373 return self.others(other).minus(self)
375 def __rtruediv__(self, scalar): # PYCHOK no cover
376 '''Not implemented.'''
377 return _NotImplemented(self, scalar)
379# def __str__(self):
380# '''Return the default C{str(self)}.
381# '''
382# return self.toStr()
384 def __sub__(self, other):
385 '''Subtract an other vector from this vector, C{this - B{other}}.
387 @arg other: The other vector (L{Vector3d}).
389 @return: Difference (L{Vector3d}).
391 @raise TypeError: Incompatible B{C{other}} C{type}.
392 '''
393 return self.minus(other)
395 def __truediv__(self, scalar):
396 '''Divide this vector by a scalar, C{this / B{scalar}}.
398 @arg scalar: The divisor (C{scalar}).
400 @return: Quotient (L{Vector3d}).
402 @raise TypeError: Non-scalar B{C{scalar}}.
403 '''
404 return self.dividedBy(scalar)
406 __trunc__ = __int__
408 if _sys_version_info2 < (3, 0): # PYCHOK no cover
409 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
410 __div__ = __truediv__
411 __idiv__ = __itruediv__
412 __long__ = __int__
413 __nonzero__ = __bool__
414 __rdiv__ = __rtruediv__
416 def angleTo(self, other, vSign=None, wrap=False):
417 '''Compute the angle between this and an other vector.
419 @arg other: The other vector (L{Vector3d}).
420 @kwarg vSign: Optional vector, if supplied (and out of the
421 plane of this and the other), angle is signed
422 positive if this->other is clockwise looking
423 along vSign or negative in opposite direction,
424 otherwise angle is unsigned.
425 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
427 @return: Angle (C{radians}).
429 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
430 '''
431 x = self.cross(other)
432 s = x.length
433 # use vSign as reference to set sign of s
434 if s and vSign and x.dot(vSign) < 0:
435 s = -s
437 a = atan2(s, self.dot(other))
438 if wrap and fabs(a) > PI:
439 a -= _copysign(PI2, a)
440 return a
442 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
443 '''Apply a 2-argument function pairwise to the components
444 of this and an other vector.
446 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
447 return a C{scalar} or L{INT0} result.
448 @arg other_x: Other X component (C{scalar}) or a vector
449 with X, Y and Z components (C{Cartesian},
450 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
451 L{Vector3Tuple} or L{Vector4Tuple}).
452 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
453 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
455 @return: New, applied vector (L{Vector3d}).
457 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
458 '''
459 if not callable(fun2):
460 raise _IsnotError(callable.__name__, fun2=fun2)
462 if fun2_kwds:
463 def _f2(a, b):
464 return fun2(a, b, **fun2_kwds)
465 else:
466 _f2 = fun2
468 xyz = _other_x_y_z3(other_x, y_z)
469 xyz = (_f2(a, b) for a, b in _zip(self.xyz, xyz)) # strict=True
470 return self.classof(*xyz)
472 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
473 '''Compute the cross product of this and an other vector.
475 @arg other: The other vector (L{Vector3d}).
476 @kwarg raiser: Optional, L{CrossError} label if raised (C{str},
477 non-L{NN}).
478 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as
479 C{x}, C{y}, and C{z}.
481 @return: Cross product (L{Vector3d}).
483 @raise CrossError: Zero or near-zero cross product and both
484 B{C{raiser}} and L{pygeodesy.crosserrors} set.
486 @raise TypeError: Incompatible B{C{other}} C{type}.
487 '''
488 other = self.others(other)
490 x = self.y * other.z - self.z * other.y
491 y = self.z * other.x - self.x * other.z
492 z = self.x * other.y - self.y * other.x
494 if raiser and self.crosserrors and eps0 > 0 \
495 and max(map1(fabs, x, y, z)) < eps0:
496 r = other._fromll or other
497 s = self._fromll or self
498 t = self.isequalTo(other, eps=eps0)
499 t = _coincident_ if t else _colinear_
500 raise CrossError(raiser, s, other=r, txt=t)
502 return self.classof(x, y, z)
504 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
505 def crosserrors(self):
506 '''Get L{CrossError} exceptions (C{bool}).
507 '''
508 return self._crosserrors
510 @crosserrors.setter # PYCHOK setter!
511 def crosserrors(self, raiser):
512 '''Raise or ignore L{CrossError} exceptions (C{bool}).
513 '''
514 self._crosserrors = bool(raiser)
516 def dividedBy(self, divisor):
517 '''Divide this vector by a scalar.
519 @arg divisor: The divisor (C{scalar}).
521 @return: New, scaled vector (L{Vector3d}).
523 @raise TypeError: Non-scalar B{C{divisor}}.
525 @raise VectorError: Invalid or zero B{C{divisor}}.
526 '''
527 d = Scalar(divisor=divisor)
528 try:
529 return self._times(_1_0 / d)
530 except (ValueError, ZeroDivisionError) as x:
531 raise VectorError(divisor=divisor, cause=x)
533 def dot(self, other):
534 '''Compute the dot (scalar) product of this and an other vector.
536 @arg other: The other vector (L{Vector3d}).
538 @return: Dot product (C{float}).
540 @raise TypeError: Incompatible B{C{other}} C{type}.
541 '''
542 return self.length2 if other is self else \
543 fdot(self.xyz, *self.others(other).xyz)
545 @deprecated_method
546 def equals(self, other, units=False): # PYCHOK no cover
547 '''DEPRECATED, use method C{isequalTo}.
548 '''
549 return self.isequalTo(other, units=units)
551 @Property_RO
552 def euclid(self):
553 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
555 @see: Properties C{length} and C{length2} and function
556 L{pygeodesy.euclid_}.
557 '''
558 return Float(euclid=euclid_(self.x, self.y, self.z))
560 def equirectangular(self, other):
561 '''I{Approximate} the different between this and an other vector.
563 @arg other: Vector to subtract (C{Vector3dBase}).
565 @return: The lenght I{squared} of the difference (C{Float}).
567 @raise TypeError: Incompatible B{C{other}} C{type}.
569 @see: Property C{length2}.
570 '''
571 d = self.minus(other)
572 return Float(equirectangular=hypot2_(d.x, d.y, d.z))
574 @Property
575 def _fromll(self):
576 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
577 '''
578 return self._ll
580 @_fromll.setter # PYCHOK setter!
581 def _fromll(self, ll):
582 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
583 '''
584 self._ll = ll or None
586 @property_RO
587 def homogeneous(self):
588 '''Get this vector's homogeneous representation (L{Vector3d}).
589 '''
590 z = self.z
591 if z:
592 x = self.x / z
593 y = self.y / z
594# z = _1_0
595 elif isneg0(z):
596 x = _copysignINF(-self.x)
597 y = _copysignINF(-self.y)
598# z = NAN
599 else:
600 x = _copysignINF(self.x)
601 y = _copysignINF(self.y)
602# z = NAN
603 return self.classof(x, y, _1_0)
605 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
606 '''Locate the vector at a given fraction between (or along) this
607 and an other vector.
609 @arg other: The other vector (L{Vector3d}).
610 @arg fraction: Fraction between both vectors (C{scalar},
611 0.0 for this and 1.0 for the other vector).
613 @return: Intermediate vector (L{Vector3d}).
615 @raise TypeError: Incompatible B{C{other}} C{type}.
616 '''
617 f = Scalar(fraction=fraction)
618 if isnear0(f): # PYCHOK no cover
619 r = self
620 else:
621 r = self.others(other)
622 if not isnear1(f): # self * (1 - f) + r * f
623 r = self.plus(r.minus(self)._times(f))
624 return r
626 def isconjugateTo(self, other, minum=1, eps=EPS):
627 '''Determine whether this and an other vector are conjugates.
629 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
630 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
631 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
632 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
633 same units as C{x}, C{y}, and C{z}.
635 @return: C{True} if both vector's components either match
636 or at least C{B{minum}} have opposite signs.
638 @raise TypeError: Incompatible B{C{other}} C{type}.
640 @see: Method C{isequalTo}.
641 '''
642 self.others(other)
643 n = 0
644 for a, b in zip(self.xyz, other.xyz):
645 if fabs(a + b) < eps and ((a < 0 and b > 0) or
646 (a > 0 and b < 0)):
647 n += 1 # conjugate
648 elif fabs(a - b) > eps:
649 return False # unequal
650 return bool(n >= minum)
652 def isequalTo(self, other, units=False, eps=EPS):
653 '''Check if this and an other vector are equal or equivalent.
655 @arg other: The other vector (L{Vector3d}).
656 @kwarg units: Optionally, compare the normalized, unit
657 version of both vectors.
658 @kwarg eps: Tolerance for equality (C{scalar}), same units as
659 C{x}, C{y}, and C{z}.
661 @return: C{True} if vectors are identical, C{False} otherwise.
663 @raise TypeError: Incompatible B{C{other}} C{type}.
665 @see: Method C{isconjugateTo}.
666 '''
667 if units:
668 self.others(other)
669 d = self.unit().minus(other.unit())
670 else:
671 d = self.minus(other)
672 return max(map(fabs, d.xyz)) < eps
674 @Property_RO
675 def length(self): # __dict__ value overwritten by Property_RO C{_united}
676 '''Get the length (norm, magnitude) of this vector (C{Float}).
678 @see: Properties L{length2} and L{euclid}.
679 '''
680 return Float(length=hypot_(self.x, self.y, self.z))
682 @Property_RO
683 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
684 '''Get the length I{squared} of this vector (C{Float}).
686 @see: Property L{length} and method C{equirectangular}.
687 '''
688 return Float(length2=hypot2_(self.x, self.y, self.z))
690 def minus(self, other):
691 '''Subtract an other vector from this vector.
693 @arg other: The other vector (L{Vector3d}).
695 @return: New vector difference (L{Vector3d}).
697 @raise TypeError: Incompatible B{C{other}} C{type}.
698 '''
699 self.others(other)
700 return self._minus(other.x, other.y, other.z)
702 def _minus(self, x, y, z):
703 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
704 '''
705 return self.classof(self.x - x, self.y - y, self.z - z)
707 def minus_(self, other_x, *y_z):
708 '''Subtract separate X, Y and Z components from this vector.
710 @arg other_x: X component (C{scalar}) or a vector's
711 X, Y, and Z components (C{Cartesian},
712 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
713 L{Vector3Tuple}, L{Vector4Tuple}).
714 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
715 ignored if B{C{other_x}} is not C{scalar}.
717 @return: New, vectiorial vector (L{Vector3d}).
719 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
720 '''
721 return self._minus(*_other_x_y_z3(other_x, y_z))
723 def negate(self):
724 '''Return this vector in opposite direction.
726 @return: New, opposite vector (L{Vector3d}).
727 '''
728 return self.classof(-self.x, -self.y, -self.z)
730 __neg__ = negate # PYCHOK no cover
732 @Property_RO
733 def _N_vector(self):
734 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
735 '''
736 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name)
738 def others(self, *other, **name_other_up):
739 '''Refined class comparison.
741 @arg other: The other vector (L{Vector3d}).
742 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
743 keyword arguments.
745 @return: The B{C{other}} if compatible.
747 @raise TypeError: Incompatible B{C{other}} C{type}.
748 '''
749 other, name, up = _xother3(self, other, **name_other_up)
750 if not isinstance(other, Vector3dBase):
751 _NamedBase.others(self, other, name=name, up=up + 1)
752 return other
754 def plus(self, other):
755 '''Add this vector and an other vector.
757 @arg other: The other vector (L{Vector3d}).
759 @return: Vectorial sum (L{Vector3d}).
761 @raise TypeError: Incompatible B{C{other}} C{type}.
762 '''
763 self.others(other)
764 return self._plus(other.x, other.y, other.z)
766 sum = plus # alternate name
768 def _plus(self, x, y, z):
769 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
770 '''
771 return self.classof(self.x + x, self.y + y, self.z + z)
773 def plus_(self, other_x, *y_z):
774 '''Sum of this vector and separate X, Y and Z components.
776 @arg other_x: X component (C{scalar}) or a vector's
777 X, Y, and Z components (C{Cartesian},
778 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
779 L{Vector3Tuple}, L{Vector4Tuple}).
780 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
781 ignored if B{C{other_x}} is not C{scalar}.
783 @return: New, vectiorial vector (L{Vector3d}).
785 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
786 '''
787 return self._plus(*_other_x_y_z3(other_x, y_z))
789 def rotate(self, axis, theta):
790 '''Rotate this vector around an axis by a specified angle.
792 @arg axis: The axis being rotated around (L{Vector3d}).
793 @arg theta: The angle of rotation (C{radians}).
795 @return: New, rotated vector (L{Vector3d}).
797 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
798 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
799 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
800 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
801 '''
802 s, c = sincos2(theta) # rotation angle
803 d = _1_0 - c
804 if d or s:
805 p = self.unit().xyz # point being rotated
806 r = self.others(axis=axis).unit() # axis being rotated around
808 ax, ay, az = r.xyz # quaternion-derived rotation matrix
809 bx, by, bz = r.times(d).xyz
810 sx, sy, sz = r.times(s).xyz
812 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)
813 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)
814 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)
815 else: # unrotated
816 x, y, z = self.xyz
817 return self.classof(x, y, z)
819 @deprecated_method
820 def rotateAround(self, axis, theta): # PYCHOK no cover
821 '''DEPRECATED, use method C{rotate}.'''
822 return self.rotate(axis, theta)
824 def times(self, factor):
825 '''Multiply this vector by a scalar.
827 @arg factor: Scale factor (C{scalar}).
829 @return: New, scaled vector (L{Vector3d}).
831 @raise TypeError: Non-scalar B{C{factor}}.
832 '''
833 return self._times(Scalar(factor=factor))
835 def _times(self, s):
836 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
837 '''
838 return self.classof(self.x * s, self.y * s, self.z * s)
840 def times_(self, other_x, *y_z):
841 '''Multiply this vector's components by separate X, Y and Z factors.
843 @arg other_x: X scale factor (C{scalar}) or a vector's
844 X, Y, and Z components as scale factors
845 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
846 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
847 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
848 ignored if B{C{other_x}} is not C{scalar}.
850 @return: New, scaled vector (L{Vector3d}).
852 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
853 '''
854 x, y, z = _other_x_y_z3(other_x, y_z)
855 return self.classof(self.x * x, self.y * y, self.z * z)
857# @deprecated_method
858# def to2ab(self): # PYCHOK no cover
859# '''DEPRECATED, use property C{Nvector.philam}.
860#
861# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
862# '''
863# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)
865# @deprecated_method
866# def to2ll(self): # PYCHOK no cover
867# '''DEPRECATED, use property C{Nvector.latlon}.
868#
869# @return: A L{LatLon2Tuple}C{(lat, lon)}.
870# '''
871# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)
873 @deprecated_method
874 def to3xyz(self): # PYCHOK no cover
875 '''DEPRECATED, use property L{xyz}.
876 '''
877 return self.xyz
879 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
880 '''Return a string representation of this vector.
882 @kwarg prec: Number of decimal places (C{int}).
883 @kwarg fmt: Enclosing format to use (C{str}).
884 @kwarg sep: Separator between components (C{str}).
886 @return: Vector as "(x, y, z)" (C{str}).
887 '''
888 t = sep.join(strs(self.xyz, prec=prec))
889 return (fmt % (t,)) if fmt else t
891 def unit(self, ll=None):
892 '''Normalize this vector to unit length.
894 @kwarg ll: Optional, original location (C{LatLon}).
896 @return: Normalized vector (L{Vector3d}).
897 '''
898 u = self._united
899 if ll:
900 u._fromll = ll
901 return u
903 @Property_RO
904 def _united(self): # __dict__ value overwritten below
905 '''(INTERNAL) Get normalized vector (L{Vector3d}).
906 '''
907 n = self.length
908 if n > EPS0 and fabs(n - _1_0) > EPS0:
909 u = self._xnamed(self.dividedBy(n))
910 u._update(False, length=_1_0, length2=_1_0, _united=u)
911 else:
912 u = self.copy()
913 u._update(False, _united=u)
914 if self._fromll:
915 u._fromll = self._fromll
916 return u
918 @Property
919 def x(self):
920 '''Get the X component (C{float}).
921 '''
922 return self._x
924 @x.setter # PYCHOK setter!
925 def x(self, x):
926 '''Set the X component, if different (C{float}).
927 '''
928 x = Float(x=x)
929 if self._x != x:
930 _update_all(self)
931 self._x = x
933 @Property
934 def xyz(self):
935 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
936 '''
937 return _MODS.namedTuples.Vector3Tuple(self.x, self.y, self.z, name=self.name)
939 @xyz.setter # PYCHOK setter!
940 def xyz(self, xyz):
941 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
942 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
943 or a C{tuple} or C{list} of 3+ C{scalar} values).
944 '''
945 if islistuple(xyz, 3):
946 self._xyz(*xyz[:3])
947 else:
948 self._xyz(xyz)
950 def _xyz(self, x_xyz, *y_z):
951 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
952 '''
953 if len(self.__dict__) > 3: # any other than initial ._x, ._y and ._z attrs
954 _update_all(self)
955 try:
956 self._x, \
957 self._y, \
958 self._z = map1(_float0, x_xyz, *y_z) if y_z else x_xyz.xyz
959 except (AttributeError, TypeError, ValueError) as x:
960 raise VectorError(cause=x, **_xyzkwds(y_z, x_xyz=x_xyz))
961 return self
963 @Property_RO
964 def x2y2z2(self):
965 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
966 '''
967 return self.x**2, self.y**2, self.z**2
969 @Property
970 def y(self):
971 '''Get the Y component (C{float}).
972 '''
973 return self._y
975 @y.setter # PYCHOK setter!
976 def y(self, y):
977 '''Set the Y component, if different (C{float}).
978 '''
979 y = Float(y=y)
980 if self._y != y:
981 _update_all(self)
982 self._y = y
984 @Property
985 def z(self):
986 '''Get the Z component (C{float}).
987 '''
988 return self._z
990 @z.setter # PYCHOK setter!
991 def z(self, z):
992 '''Set the Z component, if different (C{float}).
993 '''
994 z = Float(z=z)
995 if self._z != z:
996 _update_all(self)
997 self._z = z
1000def _other_x_y_z3(other_x, y_z):
1001 '''(INTERNAL) Helper for C{Vector3dBase.apply} and C{Vector3dBase.times_}.
1002 '''
1003 try:
1004 return map1(_float0, other_x, *y_z) if y_z else \
1005 (other_x.x, other_x.y, other_x.z) # not .xyz!
1006 except (AttributeError, TypeError, ValueError) as x:
1007 raise _InvalidError(cause=x, **_xyzkwds(y_z, other_x=other_x))
1010def _xyzkwds(y_z, **xyz): # PYCHOK no cover
1011 '''(INTERANL) Helper for C{_other_x_y_z3} and C{Vector3dBase._xyz}.
1012 '''
1013 if y_z:
1014 d = dict(_zip((_y_, _z_), y_z)) # if y_z else {}, strict=True
1015 for x in xyz.values():
1016 d.update(x=x)
1017 return d
1018 return xyz
1021__all__ += _ALL_DOCS(Vector3dBase)
1023# **) MIT License
1024#
1025# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
1026#
1027# Permission is hereby granted, free of charge, to any person obtaining a
1028# copy of this software and associated documentation files (the "Software"),
1029# to deal in the Software without restriction, including without limitation
1030# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1031# and/or sell copies of the Software, and to permit persons to whom the
1032# Software is furnished to do so, subject to the following conditions:
1033#
1034# The above copyright notice and this permission notice shall be included
1035# in all copies or substantial portions of the Software.
1036#
1037# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1038# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1039# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1040# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1041# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1042# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1043# OTHER DEALINGS IN THE SOFTWARE.