Coverage for pygeodesy/vector3dBase.py: 93%
274 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-15 16:36 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-15 16:36 -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, _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 NN, _coincident_, _colinear_, \
19 _COMMASPACE_, _xyz_
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, unstr
27from pygeodesy.units import Float, Scalar
28# from pygeodesy.utily import sincos2 # _MODS
30# from builtints import hash, int, isinstance, map, max, round, type, zip
31from math import atan2, ceil, fabs, floor, trunc
33__all__ = _ALL_LAZY.vector3dBase
34__version__ = '24.05.10'
37class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
38 '''(INTERNAL) Generic 3-D vector base class.
39 '''
40 _crosserrors = True # un/set by .errors.crosserrors
42 _ll = None # original latlon, '_fromll'
43# _x = INT0 # X component
44# _y = INT0 # Y component
45# _z = INT0 # Z component
47 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, name=NN):
48 '''New L{Vector3d} or C{Vector3dBase} instance.
50 The vector may be normalised or use x, y, z for position and
51 distance from earth centre or height relative to the surface
52 of the earth' sphere or ellipsoid.
54 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
55 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
56 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
57 C{list} of 3+ C{scalar} items).
58 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}
59 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
60 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}
61 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
62 @kwarg ll: Optional latlon reference (C{LatLon}).
63 @kwarg name: Optional name (C{str}).
65 @raise VectorError: Invalid B{C{x_xyz}}.
66 '''
67 self._x, \
68 self._y, \
69 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \
70 _xyz3(type(self), x_xyz)
71 if ll:
72 self._ll = ll
73 if name:
74 self.name = name
76 def __abs__(self):
77 '''Return the norm of this vector.
79 @return: Norm, unit length (C{float});
80 '''
81 return self.length
83 def __add__(self, other):
84 '''Add this to an other vector (L{Vector3d}).
86 @return: Vectorial sum (L{Vector3d}).
88 @raise TypeError: Incompatible B{C{other}} C{type}.
89 '''
90 return self.plus(other)
92 def __bool__(self): # PYCHOK PyChecker
93 '''Is this vector non-zero?
94 '''
95 return bool(self.x or self.y or self.z)
97 def __ceil__(self): # PYCHOK no cover
98 '''Return a vector with the C{ceil} of these components.
100 @return: Ceil-ed (L{Vector3d}).
101 '''
102 return self._mapped(ceil)
104 def __cmp__(self, other): # Python 2-
105 '''Compare this and an other vector (L{Vector3d}).
107 @return: -1, 0 or +1 (C{int}).
109 @raise TypeError: Incompatible B{C{other}} C{type}.
110 '''
111 n = self.others(other).length
112 return -1 if self.length < n else (
113 +1 if self.length > n else 0)
115 cmp = __cmp__
117 def __divmod__(self, other): # PYCHOK no cover
118 '''Not implemented.'''
119 return _NotImplemented(self, other)
121 def __eq__(self, other):
122 '''Is this vector equal to an other vector?
124 @arg other: The other vector (L{Vector3d}).
126 @return: C{True} if equal, C{False} otherwise.
128 @raise TypeError: Incompatible B{C{other}} C{type}.
129 '''
130 return self.isequalTo(other, eps=EPS0)
132 def __float__(self): # PYCHOK no cover
133 '''Not implemented.'''
134 return _NotImplemented(self)
136 def __floor__(self): # PYCHOK no cover
137 '''Return a vector with the C{floor} of these components.
139 @return: Floor-ed (L{Vector3d}).
140 '''
141 return self._mapped(floor)
143 def __floordiv__(self, other): # PYCHOK no cover
144 '''Not implemented.'''
145 return _NotImplemented(self, other)
147 def __format__(self, *other): # PYCHOK no cover
148 '''Not implemented.'''
149 return _NotImplemented(self, *other)
151 def __ge__(self, other):
152 '''Is this vector longer than or equal to an other vector?
154 @arg other: The other vector (L{Vector3d}).
156 @return: C{True} if so, C{False} otherwise.
158 @raise TypeError: Incompatible B{C{other}} C{type}.
159 '''
160 return self.length >= self.others(other).length
162# def __getitem__(self, key):
163# '''Return C{item} at index or slice C{[B{key}]}.
164# '''
165# return self.xyz[key]
167 def __gt__(self, other):
168 '''Is this vector longer than an other vector?
170 @arg other: The other vector (L{Vector3d}).
172 @return: C{True} if so, C{False} otherwise.
174 @raise TypeError: Incompatible B{C{other}} C{type}.
175 '''
176 return self.length > self.others(other).length
178 def __hash__(self): # PYCHOK no cover
179 '''Return this instance' C{hash}.
180 '''
181 return hash(self.xyz) # XXX id(self)?
183 def __iadd__(self, other):
184 '''Add this and an other vector I{in-place}, C{this += B{other}}.
186 @arg other: The other vector (L{Vector3d}).
188 @raise TypeError: Incompatible B{C{other}} C{type}.
189 '''
190 return self._xyz(self.plus(other))
192 def __ifloordiv__(self, other): # PYCHOK no cover
193 '''Not implemented.'''
194 return _NotImplemented(self, other)
196 def __imatmul__(self, other): # PYCHOK Python 3.5+
197 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
199 @arg other: The other vector (L{Vector3d}).
201 @raise TypeError: Incompatible B{C{other}} C{type}.
203 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
204 '''
205 return self._xyz(self.cross(other))
207 def __imod__(self, other): # PYCHOK no cover
208 '''Not implemented.'''
209 return _NotImplemented(self, other)
211 def __imul__(self, scalar):
212 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
214 @arg scalar: Factor (C{scalar}).
216 @raise TypeError: Non-scalar B{C{scalar}}.
217 '''
218 return self._xyz(self.times(scalar))
220 def __int__(self): # PYCHOK no cover
221 '''Return a vector with the C{int} of these components.
223 @return: Int-ed (L{Vector3d}).
224 '''
225 v = self.classof(_0_0)
226 v._x, v._y, v._z = map2(int, self.xyz)
227 return v
229 def __ipow__(self, other, *mod): # PYCHOK no cover
230 '''Not implemented.'''
231 return _NotImplemented(self, other, *mod)
233 def __isub__(self, other):
234 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
236 @arg other: The other vector (L{Vector3d}).
238 @raise TypeError: Incompatible B{C{other}} C{type}.
239 '''
240 return self._xyz(self.minus(other))
242# def __iter__(self):
243# '''Return an C{iter}ator over this vector's components.
244# '''
245# return iter(self.xyz)
247 def __itruediv__(self, scalar):
248 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
250 @arg scalar: The divisor (C{scalar}).
252 @raise TypeError: Non-scalar B{C{scalar}}.
253 '''
254 return self._xyz(self.dividedBy(scalar))
256 def __le__(self, other): # Python 3+
257 '''Is this vector shorter than or equal to an other vector?
259 @arg other: The other vector (L{Vector3d}).
261 @return: C{True} if so, C{False} otherwise.
263 @raise TypeError: Incompatible B{C{other}} C{type}.
264 '''
265 return self.length <= self.others(other).length
267# def __len__(self):
268# '''Return C{3}, always.
269# '''
270# return len(self.xyz)
272 def __lt__(self, other): # Python 3+
273 '''Is this vector shorter than an other vector?
275 @arg other: The other vector (L{Vector3d}).
277 @return: C{True} if so, C{False} otherwise.
279 @raise TypeError: Incompatible B{C{other}} C{type}.
280 '''
281 return self.length < self.others(other).length
283 def __matmul__(self, other): # PYCHOK Python 3.5+
284 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
286 @arg other: The other vector (L{Vector3d}).
288 @return: Cross product (L{Vector3d}).
290 @raise TypeError: Incompatible B{C{other}} C{type}.
291 '''
292 return self.cross(other)
294 def __mod__(self, other): # PYCHOK no cover
295 '''Not implemented.'''
296 return _NotImplemented(self, other)
298 def __mul__(self, scalar):
299 '''Multiply this vector by a scalar, C{this * B{scalar}}.
301 @arg scalar: Factor (C{scalar}).
303 @return: Product (L{Vector3d}).
304 '''
305 return self.times(scalar)
307 def __ne__(self, other):
308 '''Is this vector not equal to an other vector?
310 @arg other: The other vector (L{Vector3d}).
312 @return: C{True} if so, C{False} otherwise.
314 @raise TypeError: Incompatible B{C{other}} C{type}.
315 '''
316 return not self.isequalTo(other, eps=EPS0)
318 def __neg__(self):
319 '''Return the opposite of this vector.
321 @return: This instance negated (L{Vector3d})
322 '''
323 return self.classof(-self.x, -self.y, -self.z)
325 def __pos__(self): # PYCHOK no cover
326 '''Return this vector I{as-is} or a copy.
328 @return: This instance (L{Vector3d})
329 '''
330 return self if _pos_self else self.copy()
332 def __pow__(self, other, *mod): # PYCHOK no cover
333 '''Not implemented.'''
334 return _NotImplemented(self, other, *mod)
336 __radd__ = __add__ # PYCHOK no cover
338 def __rdivmod__ (self, other): # PYCHOK no cover
339 '''Not implemented.'''
340 return _NotImplemented(self, other)
342# def __repr__(self):
343# '''Return the default C{repr(this)}.
344# '''
345# return self.toRepr()
347 def __rfloordiv__(self, other): # PYCHOK no cover
348 '''Not implemented.'''
349 return _NotImplemented(self, other)
351 def __rmatmul__(self, other): # PYCHOK Python 3.5+
352 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
354 @arg other: The other vector (L{Vector3d}).
356 @return: Cross product (L{Vector3d}).
358 @raise TypeError: Incompatible B{C{other}} C{type}.
359 '''
360 return self.others(other).cross(self)
362 def __rmod__(self, other): # PYCHOK no cover
363 '''Not implemented.'''
364 return _NotImplemented(self, other)
366 __rmul__ = __mul__
368 def __round__(self, *ndigits): # PYCHOK no cover
369 '''Return a vector with these components C{rounded}.
371 @arg ndigits: Optional number of digits (C{int}).
373 @return: Rounded (L{Vector3d}).
374 '''
375 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
376 return self.classof(*(round(_, *ndigits) for _ in self.xyz))
378 def __rpow__(self, other, *mod): # PYCHOK no cover
379 '''Not implemented.'''
380 return _NotImplemented(self, other, *mod)
382 def __rsub__(self, other): # PYCHOK no cover
383 '''Subtract this vector from an other vector, C{B{other} - this}.
385 @arg other: The other vector (L{Vector3d}).
387 @return: Difference (L{Vector3d}).
389 @raise TypeError: Incompatible B{C{other}} C{type}.
390 '''
391 return self.others(other).minus(self)
393 def __rtruediv__(self, scalar): # PYCHOK no cover
394 '''Not implemented.'''
395 return _NotImplemented(self, scalar)
397# def __str__(self):
398# '''Return the default C{str(self)}.
399# '''
400# return self.toStr()
402 def __sub__(self, other):
403 '''Subtract an other vector from this vector, C{this - B{other}}.
405 @arg other: The other vector (L{Vector3d}).
407 @return: Difference (L{Vector3d}).
409 @raise TypeError: Incompatible B{C{other}} C{type}.
410 '''
411 return self.minus(other)
413 def __truediv__(self, scalar):
414 '''Divide this vector by a scalar, C{this / B{scalar}}.
416 @arg scalar: The divisor (C{scalar}).
418 @return: Quotient (L{Vector3d}).
420 @raise TypeError: Non-scalar B{C{scalar}}.
421 '''
422 return self.dividedBy(scalar)
424 def __trunc__(self): # PYCHOK no cover
425 '''Return a vector with the C{trunc} of these components.
427 @return: Trunc-ed (L{Vector3d}).
428 '''
429 return self._mapped(trunc)
431 if _sys_version_info2 < (3, 0): # PYCHOK no cover
432 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
433 __div__ = __truediv__
434 __idiv__ = __itruediv__
435 __long__ = __int__
436 __nonzero__ = __bool__
437 __rdiv__ = __rtruediv__
439 def angleTo(self, other, vSign=None, wrap=False):
440 '''Compute the angle between this and an other vector.
442 @arg other: The other vector (L{Vector3d}).
443 @kwarg vSign: Optional vector, if supplied (and out of the
444 plane of this and the other), angle is signed
445 positive if this->other is clockwise looking
446 along vSign or negative in opposite direction,
447 otherwise angle is unsigned.
448 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
450 @return: Angle (C{radians}).
452 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
453 '''
454 x = self.cross(other)
455 s = x.length
456 # use vSign as reference to set sign of s
457 if s and vSign and x.dot(vSign) < 0:
458 s = -s
460 a = atan2(s, self.dot(other))
461 if wrap and fabs(a) > PI:
462 a -= _copysign(PI2, a)
463 return a
465 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
466 '''Apply a 2-argument function pairwise to the components
467 of this and an other vector.
469 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
470 return a C{scalar} or L{INT0} result.
471 @arg other_x: Other X component (C{scalar}) or a vector
472 with X, Y and Z components (C{Cartesian},
473 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
474 L{Vector3Tuple} or L{Vector4Tuple}).
475 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
476 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
478 @return: New, applied vector (L{Vector3d}).
480 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
481 '''
482 _xcallable(fun2=fun2)
483 if fun2_kwds:
484 def _f2(a, b):
485 return fun2(a, b, **fun2_kwds)
486 else:
487 _f2 = fun2
489 xyz = _xyz3(self.apply, other_x, *y_z)
490 xyz = (_f2(a, b) for a, b in _zip(self.xyz, xyz)) # strict=True
491 return self.classof(*xyz)
493 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
494 '''Compute the cross product of this and an other vector.
496 @arg other: The other vector (L{Vector3d}).
497 @kwarg raiser: Optional, L{CrossError} label if raised (C{str},
498 non-L{NN}).
499 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as
500 C{x}, C{y}, and C{z}.
502 @return: Cross product (L{Vector3d}).
504 @raise CrossError: Zero or near-zero cross product and both
505 B{C{raiser}} and L{pygeodesy.crosserrors} set.
507 @raise TypeError: Incompatible B{C{other}} C{type}.
508 '''
509 X, Y, Z = self.others(other).xyz
510 x, y, z = self.xyz
511 xyz = ((y * Z - Y * z),
512 (z * X - Z * x),
513 (x * Y - X * y))
515 if raiser and self.crosserrors and eps0 > 0 \
516 and max(map(fabs, xyz)) < eps0:
517 r = other._fromll or other
518 s = self._fromll or self
519 t = self.isequalTo(other, eps=eps0)
520 t = _coincident_ if t else _colinear_
521 raise CrossError(raiser, s, other=r, txt=t)
523 return self.classof(*xyz)
525 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
526 def crosserrors(self):
527 '''Get L{CrossError} exceptions (C{bool}).
528 '''
529 return self._crosserrors
531 @crosserrors.setter # PYCHOK setter!
532 def crosserrors(self, raiser):
533 '''Raise or ignore L{CrossError} exceptions (C{bool}).
534 '''
535 self._crosserrors = bool(raiser)
537 def dividedBy(self, divisor):
538 '''Divide this vector by a scalar.
540 @arg divisor: The divisor (C{scalar}).
542 @return: New, scaled vector (L{Vector3d}).
544 @raise TypeError: Non-scalar B{C{divisor}}.
546 @raise VectorError: Invalid or zero B{C{divisor}}.
547 '''
548 d = Scalar(divisor=divisor)
549 try:
550 return self._times(_1_0 / d)
551 except (ValueError, ZeroDivisionError) as x:
552 raise VectorError(divisor=divisor, cause=x)
554 def dot(self, other):
555 '''Compute the dot (scalar) product of this and an other vector.
557 @arg other: The other vector (L{Vector3d}).
559 @return: Dot product (C{float}).
561 @raise TypeError: Incompatible B{C{other}} C{type}.
562 '''
563 return self.length2 if other is self else \
564 fdot(self.xyz, *self.others(other).xyz)
566 @deprecated_method
567 def equals(self, other, units=False): # PYCHOK no cover
568 '''DEPRECATED, use method C{isequalTo}.
569 '''
570 return self.isequalTo(other, units=units)
572 @Property_RO
573 def euclid(self):
574 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
576 @see: Properties C{length} and C{length2} and function
577 L{pygeodesy.euclid_}.
578 '''
579 return Float(euclid=euclid_(self.x, self.y, self.z))
581 def equirectangular(self, other):
582 '''I{Approximate} the different between this and an other vector.
584 @arg other: Vector to subtract (C{Vector3dBase}).
586 @return: The lenght I{squared} of the difference (C{Float}).
588 @raise TypeError: Incompatible B{C{other}} C{type}.
590 @see: Property C{length2}.
591 '''
592 d = self.minus(other)
593 return Float(equirectangular=hypot2_(d.x, d.y, d.z))
595 @Property
596 def _fromll(self):
597 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
598 '''
599 return self._ll
601 @_fromll.setter # PYCHOK setter!
602 def _fromll(self, ll):
603 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
604 '''
605 self._ll = ll or None
607 @property_RO
608 def homogeneous(self):
609 '''Get this vector's homogeneous representation (L{Vector3d}).
610 '''
611 x, y, z = self.xyz
612 if z:
613 x = x / z # /= chokes PyChecker
614 y = y / z
615# z = _1_0
616 else:
617 if isneg0(z):
618 x = -x
619 y = -y
620 x = _copysignINF(x)
621 y = _copysignINF(y)
622# z = NAN
623 return self.classof(x, y, _1_0)
625 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
626 '''Locate the vector at a given fraction between (or along) this
627 and an other vector.
629 @arg other: The other vector (L{Vector3d}).
630 @arg fraction: Fraction between both vectors (C{scalar},
631 0.0 for this and 1.0 for the other vector).
633 @return: Intermediate vector (L{Vector3d}).
635 @raise TypeError: Incompatible B{C{other}} C{type}.
636 '''
637 f = Scalar(fraction=fraction)
638 if isnear0(f): # PYCHOK no cover
639 r = self
640 else:
641 r = self.others(other)
642 if not isnear1(f): # self * (1 - f) + r * f
643 r = self.plus(r.minus(self)._times(f))
644 return r
646 def isconjugateTo(self, other, minum=1, eps=EPS):
647 '''Determine whether this and an other vector are conjugates.
649 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
650 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
651 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
652 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
653 same units as C{x}, C{y}, and C{z}.
655 @return: C{True} if both vector's components either match
656 or at least C{B{minum}} have opposite signs.
658 @raise TypeError: Incompatible B{C{other}} C{type}.
660 @see: Method C{isequalTo}.
661 '''
662 self.others(other)
663 n = 0
664 for a, b in zip(self.xyz, other.xyz):
665 if fabs(a + b) < eps and ((a < 0 and b > 0) or
666 (a > 0 and b < 0)):
667 n += 1 # conjugate
668 elif fabs(a - b) > eps:
669 return False # unequal
670 return bool(n >= minum)
672 def isequalTo(self, other, units=False, eps=EPS):
673 '''Check if this and an other vector are equal or equivalent.
675 @arg other: The other vector (L{Vector3d}).
676 @kwarg units: Optionally, compare the normalized, unit
677 version of both vectors.
678 @kwarg eps: Tolerance for equality (C{scalar}), same units as
679 C{x}, C{y}, and C{z}.
681 @return: C{True} if vectors are identical, C{False} otherwise.
683 @raise TypeError: Incompatible B{C{other}} C{type}.
685 @see: Method C{isconjugateTo}.
686 '''
687 if units:
688 self.others(other)
689 d = self.unit().minus(other.unit())
690 else:
691 d = self.minus(other)
692 return max(map(fabs, d.xyz)) < eps
694 @Property_RO
695 def length(self): # __dict__ value overwritten by Property_RO C{_united}
696 '''Get the length (norm, magnitude) of this vector (C{Float}).
698 @see: Properties L{length2} and L{euclid}.
699 '''
700 return Float(length=hypot_(self.x, self.y, self.z))
702 @Property_RO
703 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
704 '''Get the length I{squared} of this vector (C{Float}).
706 @see: Property L{length} and method C{equirectangular}.
707 '''
708 return Float(length2=hypot2_(self.x, self.y, self.z))
710 def _mapped(self, func):
711 '''(INTERNAL) Map these components.
712 '''
713 return self.classof(*map2(func, self.xyz))
715 def minus(self, other):
716 '''Subtract an other vector from this vector.
718 @arg other: The other vector (L{Vector3d}).
720 @return: New vector difference (L{Vector3d}).
722 @raise TypeError: Incompatible B{C{other}} C{type}.
723 '''
724 xyz = self.others(other).xyz
725 return self._minus(*xyz)
727 def _minus(self, x, y, z):
728 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
729 '''
730 return self.classof(self.x - x, self.y - y, self.z - z)
732 def minus_(self, other_x, *y_z):
733 '''Subtract separate X, Y and Z components from this vector.
735 @arg other_x: X component (C{scalar}) or a vector's
736 X, Y, and Z components (C{Cartesian},
737 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
738 L{Vector3Tuple}, L{Vector4Tuple}).
739 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
740 ignored if B{C{other_x}} is not C{scalar}.
742 @return: New, vectiorial vector (L{Vector3d}).
744 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
745 '''
746 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
748 def negate(self):
749 '''Return this vector in opposite direction.
751 @return: New, opposite vector (L{Vector3d}).
752 '''
753 return self.classof(-self.x, -self.y, -self.z)
755 __neg__ = negate # PYCHOK no cover
757 @Property_RO
758 def _N_vector(self):
759 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
760 '''
761 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name)
763 def others(self, *other, **name_other_up):
764 '''Refined class comparison.
766 @arg other: The other vector (L{Vector3d}).
767 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
768 keyword arguments.
770 @return: The B{C{other}} if compatible.
772 @raise TypeError: Incompatible B{C{other}} C{type}.
773 '''
774 other, name, up = _xother3(self, other, **name_other_up)
775 if not isinstance(other, Vector3dBase):
776 _NamedBase.others(self, other, name=name, up=up + 1)
777 return other
779 def plus(self, other):
780 '''Add this vector and an other vector.
782 @arg other: The other vector (L{Vector3d}).
784 @return: Vectorial sum (L{Vector3d}).
786 @raise TypeError: Incompatible B{C{other}} C{type}.
787 '''
788 xyz = self.others(other).xyz
789 return self._plus(*xyz)
791 sum = plus # alternate name
793 def _plus(self, x, y, z):
794 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
795 '''
796 return self.classof(self.x + x, self.y + y, self.z + z)
798 def plus_(self, other_x, *y_z):
799 '''Sum of this vector and separate X, Y and Z components.
801 @arg other_x: X component (C{scalar}) or a vector's
802 X, Y, and Z components (C{Cartesian},
803 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
804 L{Vector3Tuple}, L{Vector4Tuple}).
805 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
806 ignored if B{C{other_x}} is not C{scalar}.
808 @return: New, vectiorial vector (L{Vector3d}).
810 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
811 '''
812 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
814 def rotate(self, axis, theta):
815 '''Rotate this vector around an axis by a specified angle.
817 @arg axis: The axis being rotated around (L{Vector3d}).
818 @arg theta: The angle of rotation (C{radians}).
820 @return: New, rotated vector (L{Vector3d}).
822 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
823 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
824 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
825 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
826 '''
827 s, c = _MODS.utily.sincos2(theta) # rotation angle
828 d = _1_0 - c
829 if d or s:
830 p = self.unit().xyz # point being rotated
831 r = self.others(axis=axis).unit() # axis being rotated around
833 ax, ay, az = r.xyz # quaternion-derived rotation matrix
834 bx, by, bz = r.times(d).xyz
835 sx, sy, sz = r.times(s).xyz
837 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)
838 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)
839 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)
840 else: # unrotated
841 x, y, z = self.xyz
842 return self.classof(x, y, z)
844 @deprecated_method
845 def rotateAround(self, axis, theta): # PYCHOK no cover
846 '''DEPRECATED, use method C{rotate}.'''
847 return self.rotate(axis, theta)
849 def times(self, factor):
850 '''Multiply this vector by a scalar.
852 @arg factor: Scale factor (C{scalar}).
854 @return: New, scaled vector (L{Vector3d}).
856 @raise TypeError: Non-scalar B{C{factor}}.
857 '''
858 return self._times(Scalar(factor=factor))
860 def _times(self, s):
861 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
862 '''
863 return self.classof(self.x * s, self.y * s, self.z * s)
865 def times_(self, other_x, *y_z):
866 '''Multiply this vector's components by separate X, Y and Z factors.
868 @arg other_x: X scale factor (C{scalar}) or a vector's
869 X, Y, and Z components as scale factors
870 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
871 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
872 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
873 ignored if B{C{other_x}} is not C{scalar}.
875 @return: New, scaled vector (L{Vector3d}).
877 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
878 '''
879 x, y, z = _xyz3(self.times_, other_x, *y_z)
880 return self.classof(self.x * x, self.y * y, self.z * z)
882# @deprecated_method
883# def to2ab(self): # PYCHOK no cover
884# '''DEPRECATED, use property C{Nvector.philam}.
885#
886# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
887# '''
888# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)
890# @deprecated_method
891# def to2ll(self): # PYCHOK no cover
892# '''DEPRECATED, use property C{Nvector.latlon}.
893#
894# @return: A L{LatLon2Tuple}C{(lat, lon)}.
895# '''
896# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)
898 @deprecated_method
899 def to3xyz(self): # PYCHOK no cover
900 '''DEPRECATED, use property L{xyz}.
901 '''
902 return self.xyz
904 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
905 '''Return a string representation of this vector.
907 @kwarg prec: Number of decimal places (C{int}).
908 @kwarg fmt: Enclosing format to use (C{str}).
909 @kwarg sep: Separator between components (C{str}).
911 @return: Vector as "(x, y, z)" (C{str}).
912 '''
913 t = sep.join(strs(self.xyz, prec=prec))
914 return (fmt % (t,)) if fmt else t
916 def unit(self, ll=None):
917 '''Normalize this vector to unit length.
919 @kwarg ll: Optional, original location (C{LatLon}).
921 @return: Normalized vector (L{Vector3d}).
922 '''
923 u = self._united
924 if ll:
925 u._fromll = ll
926 return u
928 @Property_RO
929 def _united(self): # __dict__ value overwritten below
930 '''(INTERNAL) Get normalized vector (L{Vector3d}).
931 '''
932 n = self.length
933 if n > EPS0 and fabs(n - _1_0) > EPS0:
934 u = self._xnamed(self.dividedBy(n))
935 u._update(False, length=_1_0, length2=_1_0, _united=u)
936 else:
937 u = self.copy()
938 u._update(False, _united=u)
939 if self._fromll:
940 u._fromll = self._fromll
941 return u
943 @Property
944 def x(self):
945 '''Get the X component (C{float}).
946 '''
947 return self._x
949 @x.setter # PYCHOK setter!
950 def x(self, x):
951 '''Set the X component, if different (C{float}).
952 '''
953 x = Float(x=x)
954 if self._x != x:
955 _update_all(self, needed=3)
956 self._x = x
958 @Property
959 def xyz(self):
960 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
961 '''
962 return _MODS.namedTuples.Vector3Tuple(self.x, self.y, self.z, name=self.name)
964 @xyz.setter # PYCHOK setter!
965 def xyz(self, xyz):
966 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
967 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
968 or a C{tuple} or C{list} of 3+ C{scalar} items).
969 '''
970 self._xyz(xyz)
972 def _xyz(self, x_xyz, *y_z):
973 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
974 '''
975 _update_all(self, needed=3)
976 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z)
977 return self
979 @property_RO
980 def x2y2z2(self):
981 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
982 '''
983 return self.x**2, self.y**2, self.z**2
985 @Property
986 def y(self):
987 '''Get the Y component (C{float}).
988 '''
989 return self._y
991 @y.setter # PYCHOK setter!
992 def y(self, y):
993 '''Set the Y component, if different (C{float}).
994 '''
995 y = Float(y=y)
996 if self._y != y:
997 _update_all(self, needed=3)
998 self._y = y
1000 @Property
1001 def z(self):
1002 '''Get the Z component (C{float}).
1003 '''
1004 return self._z
1006 @z.setter # PYCHOK setter!
1007 def z(self, z):
1008 '''Set the Z component, if different (C{float}).
1009 '''
1010 z = Float(z=z)
1011 if self._z != z:
1012 _update_all(self, needed=3)
1013 self._z = z
1016def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1017 '''(INTERNAL) Helper for C{Vector3dBase.__init__}, C{-.apply}, C{-.times_} and C{-._xyz}.
1018 '''
1019 try:
1020 x_y_z = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for VectorXTuple
1021 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1022 x_xyz.xyz)
1023 except (AttributeError, TypeError, ValueError) as x:
1024 raise _xError(x, unstr(where, x_xyz, *y_z))
1025 return x_y_z
1028__all__ += _ALL_DOCS(Vector3dBase)
1030# **) MIT License
1031#
1032# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1033#
1034# Permission is hereby granted, free of charge, to any person obtaining a
1035# copy of this software and associated documentation files (the "Software"),
1036# to deal in the Software without restriction, including without limitation
1037# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1038# and/or sell copies of the Software, and to permit persons to whom the
1039# Software is furnished to do so, subject to the following conditions:
1040#
1041# The above copyright notice and this permission notice shall be included
1042# in all copies or substantial portions of the Software.
1043#
1044# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1045# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1046# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1047# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1048# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1049# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1050# OTHER DEALINGS IN THE SOFTWARE.