Coverage for pygeodesy/vector3dBase.py: 93%
275 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-10 14:08 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-10 14:08 -0400
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, \
12 map2, _signOf, _zip
13from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, _copysignINF, \
14 _float0, isnear0, isnear1, isneg0, \
15 _pos_self, _0_0, _1_0
16from pygeodesy.errors import CrossError, VectorError, _xcallable, _xError
17from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_
18from pygeodesy.interns import _coincident_, _colinear_, _COMMASPACE_, _xyz_
19from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS, \
20 _sys_version_info2
21from pygeodesy.named import _NamedBase, _NotImplemented, _xother3
22# from pygeodesy.namedTuples import Vector3Tuple # _MODS
23from pygeodesy.props import deprecated_method, Property, Property_RO, \
24 property_doc_, property_RO, _update_all
25from pygeodesy.streprs import Fmt, strs, unstr
26from pygeodesy.units import Float, Scalar
27# from pygeodesy.utily import sincos2 # _MODS
29# from builtints import hash, int, isinstance, map, max, round, type, zip
30from math import atan2, ceil, fabs, floor, trunc
32__all__ = _ALL_LAZY.vector3dBase
33__version__ = '24.06.07'
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):
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} 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 C{B{name}=NN} (C{str}).
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
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 '''Return a vector with the C{ceil} of these components.
99 @return: Ceil-ed (L{Vector3d}).
100 '''
101 return self._mapped(ceil)
103 def __cmp__(self, other): # Python 2-
104 '''Compare this and an other vector (L{Vector3d}).
106 @return: -1, 0 or +1 (C{int}).
108 @raise TypeError: Incompatible B{C{other}} C{type}.
109 '''
110 return _signOf(self.length, self._other_cmp(other))
112 cmp = __cmp__
114 def __divmod__(self, other): # PYCHOK no cover
115 '''Not implemented.'''
116 return _NotImplemented(self, other)
118 def __eq__(self, other):
119 '''Is this vector equal to an other vector?
121 @arg other: The other vector (L{Vector3d}).
123 @return: C{True} if equal, C{False} otherwise.
125 @raise TypeError: Incompatible B{C{other}} C{type}.
126 '''
127 return self.isequalTo(other, eps=EPS0)
129 def __float__(self): # PYCHOK no cover
130 '''Not implemented.'''
131 return _NotImplemented(self)
133 def __floor__(self): # PYCHOK no cover
134 '''Return a vector with the C{floor} of these components.
136 @return: Floor-ed (L{Vector3d}).
137 '''
138 return self._mapped(floor)
140 def __floordiv__(self, other): # PYCHOK no cover
141 '''Not implemented.'''
142 return _NotImplemented(self, other)
144 def __format__(self, *other): # PYCHOK no cover
145 '''Not implemented.'''
146 return _NotImplemented(self, *other)
148 def __ge__(self, other):
149 '''Is this vector longer than or equal to an other vector?
151 @arg other: The other vector (L{Vector3d}).
153 @return: C{True} if so, C{False} otherwise.
155 @raise TypeError: Incompatible B{C{other}} C{type}.
156 '''
157 return self.length >= self._other_cmp(other)
159# def __getitem__(self, key):
160# '''Return C{item} at index or slice C{[B{key}]}.
161# '''
162# return self.xyz[key]
164 def __gt__(self, other):
165 '''Is this vector longer than an other vector?
167 @arg other: The other vector (L{Vector3d}).
169 @return: C{True} if so, C{False} otherwise.
171 @raise TypeError: Incompatible B{C{other}} C{type}.
172 '''
173 return self.length > self._other_cmp(other)
175 def __hash__(self): # PYCHOK no cover
176 '''Return this instance' C{hash}.
177 '''
178 return hash(self.xyz) # XXX id(self)?
180 def __iadd__(self, other):
181 '''Add this and an other vector I{in-place}, C{this += B{other}}.
183 @arg other: The other vector (L{Vector3d}).
185 @raise TypeError: Incompatible B{C{other}} C{type}.
186 '''
187 return self._xyz(self.plus(other))
189 def __ifloordiv__(self, other): # PYCHOK no cover
190 '''Not implemented.'''
191 return _NotImplemented(self, other)
193 def __imatmul__(self, other): # PYCHOK Python 3.5+
194 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
196 @arg other: The other vector (L{Vector3d}).
198 @raise TypeError: Incompatible B{C{other}} C{type}.
200 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
201 '''
202 return self._xyz(self.cross(other))
204 def __imod__(self, other): # PYCHOK no cover
205 '''Not implemented.'''
206 return _NotImplemented(self, other)
208 def __imul__(self, scalar):
209 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
211 @arg scalar: Factor (C{scalar}).
213 @raise TypeError: Non-scalar B{C{scalar}}.
214 '''
215 return self._xyz(self.times(scalar))
217 def __int__(self): # PYCHOK no cover
218 '''Return a vector with the C{int} of these components.
220 @return: Int-ed (L{Vector3d}).
221 '''
222 v = self.classof(_0_0)
223 v._x, v._y, v._z = map2(int, self.xyz)
224 return v
226 def __ipow__(self, other, *mod): # PYCHOK no cover
227 '''Not implemented.'''
228 return _NotImplemented(self, other, *mod)
230 def __isub__(self, other):
231 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
233 @arg other: The other vector (L{Vector3d}).
235 @raise TypeError: Incompatible B{C{other}} C{type}.
236 '''
237 return self._xyz(self.minus(other))
239# def __iter__(self):
240# '''Return an C{iter}ator over this vector's components.
241# '''
242# return iter(self.xyz)
244 def __itruediv__(self, scalar):
245 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
247 @arg scalar: The divisor (C{scalar}).
249 @raise TypeError: Non-scalar B{C{scalar}}.
250 '''
251 return self._xyz(self.dividedBy(scalar))
253 def __le__(self, other): # Python 3+
254 '''Is this vector shorter than or equal to an other vector?
256 @arg other: The other vector (L{Vector3d}).
258 @return: C{True} if so, C{False} otherwise.
260 @raise TypeError: Incompatible B{C{other}} C{type}.
261 '''
262 return self.length <= self._other_cmp(other)
264# def __len__(self):
265# '''Return C{3}, always.
266# '''
267# return len(self.xyz)
269 def __lt__(self, other): # Python 3+
270 '''Is this vector shorter than an other vector?
272 @arg other: The other vector (L{Vector3d}).
274 @return: C{True} if so, C{False} otherwise.
276 @raise TypeError: Incompatible B{C{other}} C{type}.
277 '''
278 return self.length < self._other_cmp(other)
280 def __matmul__(self, other): # PYCHOK Python 3.5+
281 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
283 @arg other: The other vector (L{Vector3d}).
285 @return: Cross product (L{Vector3d}).
287 @raise TypeError: Incompatible B{C{other}} C{type}.
288 '''
289 return self.cross(other)
291 def __mod__(self, other): # PYCHOK no cover
292 '''Not implemented.'''
293 return _NotImplemented(self, other)
295 def __mul__(self, scalar):
296 '''Multiply this vector by a scalar, C{this * B{scalar}}.
298 @arg scalar: Factor (C{scalar}).
300 @return: Product (L{Vector3d}).
301 '''
302 return self.times(scalar)
304 def __ne__(self, other):
305 '''Is this vector not equal to an other vector?
307 @arg other: The other vector (L{Vector3d}).
309 @return: C{True} if so, C{False} otherwise.
311 @raise TypeError: Incompatible B{C{other}} C{type}.
312 '''
313 return not self.isequalTo(other, eps=EPS0)
315 def __neg__(self):
316 '''Return the opposite of this vector.
318 @return: This instance negated (L{Vector3d})
319 '''
320 return self.classof(-self.x, -self.y, -self.z)
322 def __pos__(self): # PYCHOK no cover
323 '''Return this vector I{as-is} or a copy.
325 @return: This instance (L{Vector3d})
326 '''
327 return self if _pos_self else self.copy()
329 def __pow__(self, other, *mod): # PYCHOK no cover
330 '''Not implemented.'''
331 return _NotImplemented(self, other, *mod)
333 __radd__ = __add__ # PYCHOK no cover
335 def __rdivmod__ (self, other): # PYCHOK no cover
336 '''Not implemented.'''
337 return _NotImplemented(self, other)
339# def __repr__(self):
340# '''Return the default C{repr(this)}.
341# '''
342# return self.toRepr()
344 def __rfloordiv__(self, other): # PYCHOK no cover
345 '''Not implemented.'''
346 return _NotImplemented(self, other)
348 def __rmatmul__(self, other): # PYCHOK Python 3.5+
349 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
351 @arg other: The other vector (L{Vector3d}).
353 @return: Cross product (L{Vector3d}).
355 @raise TypeError: Incompatible B{C{other}} C{type}.
356 '''
357 return self.others(other).cross(self)
359 def __rmod__(self, other): # PYCHOK no cover
360 '''Not implemented.'''
361 return _NotImplemented(self, other)
363 __rmul__ = __mul__
365 def __round__(self, *ndigits): # PYCHOK no cover
366 '''Return a vector with these components C{rounded}.
368 @arg ndigits: Optional number of digits (C{int}).
370 @return: Rounded (L{Vector3d}).
371 '''
372 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
373 return self.classof(*(round(_, *ndigits) for _ in self.xyz))
375 def __rpow__(self, other, *mod): # PYCHOK no cover
376 '''Not implemented.'''
377 return _NotImplemented(self, other, *mod)
379 def __rsub__(self, other): # PYCHOK no cover
380 '''Subtract this vector from an other vector, C{B{other} - this}.
382 @arg other: The other vector (L{Vector3d}).
384 @return: Difference (L{Vector3d}).
386 @raise TypeError: Incompatible B{C{other}} C{type}.
387 '''
388 return self.others(other).minus(self)
390 def __rtruediv__(self, scalar): # PYCHOK no cover
391 '''Not implemented.'''
392 return _NotImplemented(self, scalar)
394# def __str__(self):
395# '''Return the default C{str(self)}.
396# '''
397# return self.toStr()
399 def __sub__(self, other):
400 '''Subtract an other vector from this vector, C{this - B{other}}.
402 @arg other: The other vector (L{Vector3d}).
404 @return: Difference (L{Vector3d}).
406 @raise TypeError: Incompatible B{C{other}} C{type}.
407 '''
408 return self.minus(other)
410 def __truediv__(self, scalar):
411 '''Divide this vector by a scalar, C{this / B{scalar}}.
413 @arg scalar: The divisor (C{scalar}).
415 @return: Quotient (L{Vector3d}).
417 @raise TypeError: Non-scalar B{C{scalar}}.
418 '''
419 return self.dividedBy(scalar)
421 def __trunc__(self): # PYCHOK no cover
422 '''Return a vector with the C{trunc} of these components.
424 @return: Trunc-ed (L{Vector3d}).
425 '''
426 return self._mapped(trunc)
428 if _sys_version_info2 < (3, 0): # PYCHOK no cover
429 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
430 __div__ = __truediv__
431 __idiv__ = __itruediv__
432 __long__ = __int__
433 __nonzero__ = __bool__
434 __rdiv__ = __rtruediv__
436 def angleTo(self, other, vSign=None, wrap=False):
437 '''Compute the angle between this and an other vector.
439 @arg other: The other vector (L{Vector3d}).
440 @kwarg vSign: Optional vector, if supplied (and out of the
441 plane of this and the other), angle is signed
442 positive if this->other is clockwise looking
443 along vSign or negative in opposite direction,
444 otherwise angle is unsigned.
445 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
447 @return: Angle (C{radians}).
449 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
450 '''
451 x = self.cross(other)
452 s = x.length
453 # use vSign as reference to set sign of s
454 if s and vSign and x.dot(vSign) < 0:
455 s = -s
457 a = atan2(s, self.dot(other))
458 if wrap and fabs(a) > PI:
459 a -= _copysign(PI2, a)
460 return a
462 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
463 '''Apply a 2-argument function pairwise to the components
464 of this and an other vector.
466 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
467 return a C{scalar} or L{INT0} result.
468 @arg other_x: Other X component (C{scalar}) or a vector
469 with X, Y and Z components (C{Cartesian},
470 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
471 L{Vector3Tuple} or L{Vector4Tuple}).
472 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
473 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
475 @return: New, applied vector (L{Vector3d}).
477 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
478 '''
479 _xcallable(fun2=fun2)
480 if fun2_kwds:
481 def _f2(a, b):
482 return fun2(a, b, **fun2_kwds)
483 else:
484 _f2 = fun2
486 xyz = _xyz3(self.apply, other_x, *y_z)
487 xyz = (_f2(a, b) for a, b in _zip(self.xyz, xyz)) # strict=True
488 return self.classof(*xyz)
490 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
491 '''Compute the cross product of this and an other vector.
493 @arg other: The other vector (L{Vector3d}).
494 @kwarg raiser: Optional, L{CrossError} label if raised (C{str},
495 non-L{NN}).
496 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as
497 C{x}, C{y}, and C{z}.
499 @return: Cross product (L{Vector3d}).
501 @raise CrossError: Zero or near-zero cross product and both
502 B{C{raiser}} and L{pygeodesy.crosserrors} set.
504 @raise TypeError: Incompatible B{C{other}} C{type}.
505 '''
506 X, Y, Z = self.others(other).xyz
507 x, y, z = self.xyz
508 xyz = ((y * Z - Y * z),
509 (z * X - Z * x),
510 (x * Y - X * y))
512 if raiser and self.crosserrors and eps0 > 0 \
513 and max(map(fabs, xyz)) < eps0:
514 r = other._fromll or other
515 s = self._fromll or self
516 t = self.isequalTo(other, eps=eps0)
517 t = _coincident_ if t else _colinear_
518 raise CrossError(raiser, s, other=r, txt=t)
520 return self.classof(*xyz)
522 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
523 def crosserrors(self):
524 '''Get L{CrossError} exceptions (C{bool}).
525 '''
526 return self._crosserrors
528 @crosserrors.setter # PYCHOK setter!
529 def crosserrors(self, raiser):
530 '''Raise or ignore L{CrossError} exceptions (C{bool}).
531 '''
532 self._crosserrors = bool(raiser)
534 def dividedBy(self, divisor):
535 '''Divide this vector by a scalar.
537 @arg divisor: The divisor (C{scalar}).
539 @return: New, scaled vector (L{Vector3d}).
541 @raise TypeError: Non-scalar B{C{divisor}}.
543 @raise VectorError: Invalid or zero B{C{divisor}}.
544 '''
545 d = Scalar(divisor=divisor)
546 try:
547 return self._times(_1_0 / d)
548 except (ValueError, ZeroDivisionError) as x:
549 raise VectorError(divisor=divisor, cause=x)
551 def dot(self, other):
552 '''Compute the dot (scalar) product of this and an other vector.
554 @arg other: The other vector (L{Vector3d}).
556 @return: Dot product (C{float}).
558 @raise TypeError: Incompatible B{C{other}} C{type}.
559 '''
560 return self.length2 if other is self else \
561 fdot(self.xyz, *self.others(other).xyz)
563 @deprecated_method
564 def equals(self, other, units=False): # PYCHOK no cover
565 '''DEPRECATED, use method C{isequalTo}.
566 '''
567 return self.isequalTo(other, units=units)
569 @Property_RO
570 def euclid(self):
571 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
573 @see: Properties C{length} and C{length2} and function
574 L{pygeodesy.euclid_}.
575 '''
576 return Float(euclid=euclid_(self.x, self.y, self.z))
578 def equirectangular(self, other):
579 '''I{Approximate} the different between this and an other vector.
581 @arg other: Vector to subtract (C{Vector3dBase}).
583 @return: The lenght I{squared} of the difference (C{Float}).
585 @raise TypeError: Incompatible B{C{other}} C{type}.
587 @see: Property C{length2}.
588 '''
589 d = self.minus(other)
590 return Float(equirectangular=hypot2_(d.x, d.y, d.z))
592 @Property
593 def _fromll(self):
594 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
595 '''
596 return self._ll
598 @_fromll.setter # PYCHOK setter!
599 def _fromll(self, ll):
600 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
601 '''
602 self._ll = ll or None
604 @property_RO
605 def homogeneous(self):
606 '''Get this vector's homogeneous representation (L{Vector3d}).
607 '''
608 x, y, z = self.xyz
609 if z:
610 x = x / z # /= chokes PyChecker
611 y = y / z
612# z = _1_0
613 else:
614 if isneg0(z):
615 x = -x
616 y = -y
617 x = _copysignINF(x)
618 y = _copysignINF(y)
619# z = NAN
620 return self.classof(x, y, _1_0)
622 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
623 '''Locate the vector at a given fraction between (or along) this
624 and an other vector.
626 @arg other: The other vector (L{Vector3d}).
627 @arg fraction: Fraction between both vectors (C{scalar},
628 0.0 for this and 1.0 for the other vector).
630 @return: Intermediate vector (L{Vector3d}).
632 @raise TypeError: Incompatible B{C{other}} C{type}.
633 '''
634 f = Scalar(fraction=fraction)
635 if isnear0(f): # PYCHOK no cover
636 r = self
637 else:
638 r = self.others(other)
639 if not isnear1(f): # self * (1 - f) + r * f
640 r = self.plus(r.minus(self)._times(f))
641 return r
643 def isconjugateTo(self, other, minum=1, eps=EPS):
644 '''Determine whether this and an other vector are conjugates.
646 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
647 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
648 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
649 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
650 same units as C{x}, C{y}, and C{z}.
652 @return: C{True} if both vector's components either match
653 or at least C{B{minum}} have opposite signs.
655 @raise TypeError: Incompatible B{C{other}} C{type}.
657 @see: Method C{isequalTo}.
658 '''
659 self.others(other)
660 n = 0
661 for a, b in zip(self.xyz, other.xyz):
662 if fabs(a + b) < eps and ((a < 0 and b > 0) or
663 (a > 0 and b < 0)):
664 n += 1 # conjugate
665 elif fabs(a - b) > eps:
666 return False # unequal
667 return bool(n >= minum)
669 def isequalTo(self, other, units=False, eps=EPS):
670 '''Check if this and an other vector are equal or equivalent.
672 @arg other: The other vector (L{Vector3d}).
673 @kwarg units: Optionally, compare the normalized, unit
674 version of both vectors.
675 @kwarg eps: Tolerance for equality (C{scalar}), same units as
676 C{x}, C{y}, and C{z}.
678 @return: C{True} if vectors are identical, C{False} otherwise.
680 @raise TypeError: Incompatible B{C{other}} C{type}.
682 @see: Method C{isconjugateTo}.
683 '''
684 if units:
685 self.others(other)
686 d = self.unit().minus(other.unit())
687 else:
688 d = self.minus(other)
689 return max(map(fabs, d.xyz)) < eps
691 @Property_RO
692 def length(self): # __dict__ value overwritten by Property_RO C{_united}
693 '''Get the length (norm, magnitude) of this vector (C{Float}).
695 @see: Properties L{length2} and L{euclid}.
696 '''
697 return Float(length=hypot_(self.x, self.y, self.z))
699 @Property_RO
700 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
701 '''Get the length I{squared} of this vector (C{Float}).
703 @see: Property L{length} and method C{equirectangular}.
704 '''
705 return Float(length2=hypot2_(self.x, self.y, self.z))
707 def _mapped(self, func):
708 '''(INTERNAL) Map these components.
709 '''
710 return self.classof(*map2(func, self.xyz))
712 def minus(self, other):
713 '''Subtract an other vector from this vector.
715 @arg other: The other vector (L{Vector3d}).
717 @return: New vector difference (L{Vector3d}).
719 @raise TypeError: Incompatible B{C{other}} C{type}.
720 '''
721 xyz = self.others(other).xyz
722 return self._minus(*xyz)
724 def _minus(self, x, y, z):
725 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
726 '''
727 return self.classof(self.x - x, self.y - y, self.z - z)
729 def minus_(self, other_x, *y_z):
730 '''Subtract separate X, Y and Z components from this vector.
732 @arg other_x: X component (C{scalar}) or a vector's
733 X, Y, and Z components (C{Cartesian},
734 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
735 L{Vector3Tuple}, L{Vector4Tuple}).
736 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
737 ignored if B{C{other_x}} is not C{scalar}.
739 @return: New, vectiorial vector (L{Vector3d}).
741 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
742 '''
743 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
745 def negate(self):
746 '''Return this vector in opposite direction.
748 @return: New, opposite vector (L{Vector3d}).
749 '''
750 return self.classof(-self.x, -self.y, -self.z)
752 __neg__ = negate # PYCHOK no cover
754 @Property_RO
755 def _N_vector(self):
756 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
757 '''
758 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name)
760 def _other_cmp(self, other):
761 '''(INTERNAL) Return the value for comparison.
762 '''
763 return other if isscalar(other) else self.others(other).length
765 def others(self, *other, **name_other_up):
766 '''Refined class comparison.
768 @arg other: The other vector (L{Vector3d}).
769 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
770 keyword arguments.
772 @return: The B{C{other}} if compatible.
774 @raise TypeError: Incompatible B{C{other}} C{type}.
775 '''
776 other, name, up = _xother3(self, other, **name_other_up)
777 if not isinstance(other, Vector3dBase):
778 _NamedBase.others(self, other, name=name, up=up + 1)
779 return other
781 def plus(self, other):
782 '''Add this vector and an other vector.
784 @arg other: The other vector (L{Vector3d}).
786 @return: Vectorial sum (L{Vector3d}).
788 @raise TypeError: Incompatible B{C{other}} C{type}.
789 '''
790 xyz = self.others(other).xyz
791 return self._plus(*xyz)
793 sum = plus # alternate name
795 def _plus(self, x, y, z):
796 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
797 '''
798 return self.classof(self.x + x, self.y + y, self.z + z)
800 def plus_(self, other_x, *y_z):
801 '''Sum of this vector and separate X, Y and Z components.
803 @arg other_x: X component (C{scalar}) or a vector's
804 X, Y, and Z components (C{Cartesian},
805 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
806 L{Vector3Tuple}, L{Vector4Tuple}).
807 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
808 ignored if B{C{other_x}} is not C{scalar}.
810 @return: New, vectiorial vector (L{Vector3d}).
812 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
813 '''
814 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
816 def rotate(self, axis, theta):
817 '''Rotate this vector around an axis by a specified angle.
819 @arg axis: The axis being rotated around (L{Vector3d}).
820 @arg theta: The angle of rotation (C{radians}).
822 @return: New, rotated vector (L{Vector3d}).
824 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
825 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
826 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
827 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
828 '''
829 s, c = _MODS.utily.sincos2(theta) # rotation angle
830 d = _1_0 - c
831 if d or s:
832 p = self.unit().xyz # point being rotated
833 r = self.others(axis=axis).unit() # axis being rotated around
835 ax, ay, az = r.xyz # quaternion-derived rotation matrix
836 bx, by, bz = r.times(d).xyz
837 sx, sy, sz = r.times(s).xyz
839 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)
840 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)
841 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)
842 else: # unrotated
843 x, y, z = self.xyz
844 return self.classof(x, y, z)
846 @deprecated_method
847 def rotateAround(self, axis, theta): # PYCHOK no cover
848 '''DEPRECATED, use method C{rotate}.'''
849 return self.rotate(axis, theta)
851 def times(self, factor):
852 '''Multiply this vector by a scalar.
854 @arg factor: Scale factor (C{scalar}).
856 @return: New, scaled vector (L{Vector3d}).
858 @raise TypeError: Non-scalar B{C{factor}}.
859 '''
860 return self._times(Scalar(factor=factor))
862 def _times(self, s):
863 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
864 '''
865 return self.classof(self.x * s, self.y * s, self.z * s)
867 def times_(self, other_x, *y_z):
868 '''Multiply this vector's components by separate X, Y and Z factors.
870 @arg other_x: X scale factor (C{scalar}) or a vector's
871 X, Y, and Z components as scale factors
872 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
873 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
874 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
875 ignored if B{C{other_x}} is not C{scalar}.
877 @return: New, scaled vector (L{Vector3d}).
879 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
880 '''
881 x, y, z = _xyz3(self.times_, other_x, *y_z)
882 return self.classof(self.x * x, self.y * y, self.z * z)
884# @deprecated_method
885# def to2ab(self): # PYCHOK no cover
886# '''DEPRECATED, use property C{Nvector.philam}.
887#
888# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
889# '''
890# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)
892# @deprecated_method
893# def to2ll(self): # PYCHOK no cover
894# '''DEPRECATED, use property C{Nvector.latlon}.
895#
896# @return: A L{LatLon2Tuple}C{(lat, lon)}.
897# '''
898# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)
900 @deprecated_method
901 def to3xyz(self): # PYCHOK no cover
902 '''DEPRECATED, use property L{xyz}.
903 '''
904 return self.xyz
906 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
907 '''Return a string representation of this vector.
909 @kwarg prec: Number of decimal places (C{int}).
910 @kwarg fmt: Enclosing format to use (C{str}).
911 @kwarg sep: Separator between components (C{str}).
913 @return: Vector as "(x, y, z)" (C{str}).
914 '''
915 t = sep.join(strs(self.xyz, prec=prec))
916 return (fmt % (t,)) if fmt else t
918 def unit(self, ll=None):
919 '''Normalize this vector to unit length.
921 @kwarg ll: Optional, original location (C{LatLon}).
923 @return: Normalized vector (L{Vector3d}).
924 '''
925 u = self._united
926 if ll:
927 u._fromll = ll
928 return u
930 @Property_RO
931 def _united(self): # __dict__ value overwritten below
932 '''(INTERNAL) Get normalized vector (L{Vector3d}).
933 '''
934 n = self.length
935 if n > EPS0 and fabs(n - _1_0) > EPS0:
936 u = self._xnamed(self.dividedBy(n))
937 u._update(False, length=_1_0, length2=_1_0, _united=u)
938 else:
939 u = self.copy()
940 u._update(False, _united=u)
941 if self._fromll:
942 u._fromll = self._fromll
943 return u
945 @Property
946 def x(self):
947 '''Get the X component (C{float}).
948 '''
949 return self._x
951 @x.setter # PYCHOK setter!
952 def x(self, x):
953 '''Set the X component, if different (C{float}).
954 '''
955 x = Float(x=x)
956 if self._x != x:
957 _update_all(self, needed=3)
958 self._x = x
960 @Property
961 def xyz(self):
962 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
963 '''
964 return _MODS.namedTuples.Vector3Tuple(self.x, self.y, self.z, name=self.name)
966 @xyz.setter # PYCHOK setter!
967 def xyz(self, xyz):
968 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
969 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
970 or a C{tuple} or C{list} of 3+ C{scalar} items).
971 '''
972 self._xyz(xyz)
974 def _xyz(self, x_xyz, *y_z):
975 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
976 '''
977 _update_all(self, needed=3)
978 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z)
979 return self
981 @property_RO
982 def x2y2z2(self):
983 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
984 '''
985 return self.x**2, self.y**2, self.z**2
987 @Property
988 def y(self):
989 '''Get the Y component (C{float}).
990 '''
991 return self._y
993 @y.setter # PYCHOK setter!
994 def y(self, y):
995 '''Set the Y component, if different (C{float}).
996 '''
997 y = Float(y=y)
998 if self._y != y:
999 _update_all(self, needed=3)
1000 self._y = y
1002 @Property
1003 def z(self):
1004 '''Get the Z component (C{float}).
1005 '''
1006 return self._z
1008 @z.setter # PYCHOK setter!
1009 def z(self, z):
1010 '''Set the Z component, if different (C{float}).
1011 '''
1012 z = Float(z=z)
1013 if self._z != z:
1014 _update_all(self, needed=3)
1015 self._z = z
1018def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1019 '''(INTERNAL) Helper for C{Vector3dBase.__init__}, C{-.apply}, C{-.times_} and C{-._xyz}.
1020 '''
1021 try:
1022 x_y_z = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for VectorXTuple
1023 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1024 x_xyz.xyz)
1025 except (AttributeError, TypeError, ValueError) as x:
1026 raise _xError(x, unstr(where, x_xyz, *y_z))
1027 return x_y_z
1030__all__ += _ALL_DOCS(Vector3dBase)
1032# **) MIT License
1033#
1034# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1035#
1036# Permission is hereby granted, free of charge, to any person obtaining a
1037# copy of this software and associated documentation files (the "Software"),
1038# to deal in the Software without restriction, including without limitation
1039# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1040# and/or sell copies of the Software, and to permit persons to whom the
1041# Software is furnished to do so, subject to the following conditions:
1042#
1043# The above copyright notice and this permission notice shall be included
1044# in all copies or substantial portions of the Software.
1045#
1046# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1047# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1048# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1049# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1050# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1051# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1052# OTHER DEALINGS IN THE SOFTWARE.