Coverage for pygeodesy/vector3dBase.py: 96%
269 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-06-07 08:37 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-06-07 08:37 -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 copysign0, islistuple, isscalar, map1, _zip
12from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, _float0, \
13 isnear0, isnear1, _pos_self, _1_0
14from pygeodesy.errors import CrossError, _InvalidError, _IsnotError, \
15 VectorError
16from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_
17from pygeodesy.interns import NN, _coincident_, _colinear_, \
18 _COMMASPACE_, _y_, _z_
19from pygeodesy.lazily import _ALL_LAZY, _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_, _update_all
25from pygeodesy.streprs import Fmt, strs
26from pygeodesy.units import Float, Scalar
27from pygeodesy.utily import sincos2, atan2, fabs
29# from math import atan2, fabs # from .utily
31__all__ = _ALL_LAZY.vector3dBase
32__version__ = '23.05.27'
35class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
36 '''(INTERNAL) Generic 3-D vector base class.
38 In a geodesy context, these may be used to represent:
39 - n-vector representing a normal to point on earth's surface
40 - earth-centered, earth-fixed cartesian (= spherical n-vector)
41 - great circle normal to vector
42 - motion vector on earth's surface
43 - etc.
44 '''
45 _crosserrors = True # un/set by .errors.crosserrors
47 _ll = None # original latlon, '_fromll'
48 _x = INT0 # X component
49 _y = INT0 # Y component
50 _z = INT0 # Z component
52 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, name=NN):
53 '''New L{Vector3d} or C{Vector3dBase} instance.
55 The vector may be normalised or use x, y, z for position and
56 distance from earth centre or height relative to the surface
57 of the earth' sphere or ellipsoid.
59 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
60 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
61 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
62 C{list} of 3+ C{scalar} values).
63 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}
64 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
65 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}
66 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
67 @kwarg ll: Optional latlon reference (C{LatLon}).
68 @kwarg name: Optional name (C{str}).
70 @raise VectorError: Invalid B{C{x_xyz}}.
71 '''
72 if isscalar(x_xyz):
73 self._xyz(x_xyz, y, z)
74 else:
75 self.xyz = x_xyz
76 if ll:
77 self._ll = ll
78 if name:
79 self.name = name
81 def __abs__(self):
82 '''Return the norm of this vector.
84 @return: Norm, unit length (C{float});
85 '''
86 return self.length
88 def __add__(self, other):
89 '''Add this to an other vector (L{Vector3d}).
91 @return: Vectorial sum (L{Vector3d}).
93 @raise TypeError: Incompatible B{C{other}} C{type}.
94 '''
95 return self.plus(other)
97 def __bool__(self): # PYCHOK PyChecker
98 '''Is this vector non-zero?
99 '''
100 return bool(self.x or self.y or self.z)
102 def __ceil__(self): # PYCHOK no cover
103 '''Not implemented.'''
104 return _NotImplemented(self)
106 def __cmp__(self, other): # Python 2-
107 '''Compare this and an other vector (L{Vector3d}).
109 @return: -1, 0 or +1 (C{int}).
111 @raise TypeError: Incompatible B{C{other}} C{type}.
112 '''
113 n = self.others(other).length
114 return -1 if self.length < n else (
115 +1 if self.length > n else 0)
117 cmp = __cmp__
119 def __divmod__(self, other): # PYCHOK no cover
120 '''Not implemented.'''
121 return _NotImplemented(self, other)
123 def __eq__(self, other):
124 '''Is this vector equal to an other vector?
126 @arg other: The other vector (L{Vector3d}).
128 @return: C{True} if equal, C{False} otherwise.
130 @raise TypeError: Incompatible B{C{other}} C{type}.
131 '''
132 return self.isequalTo(other, eps=EPS0)
134 def __float__(self): # PYCHOK no cover
135 '''Not implemented.'''
136 return _NotImplemented(self)
138 def __floor__(self): # PYCHOK no cover
139 '''Not implemented.'''
140 return _NotImplemented(self)
142 def __floordiv__(self, other): # PYCHOK no cover
143 '''Not implemented.'''
144 return _NotImplemented(self, other)
146 def __format__(self, *other): # PYCHOK no cover
147 '''Not implemented.'''
148 return _NotImplemented(self, *other)
150 def __ge__(self, other):
151 '''Is this vector longer than or equal to an other vector?
153 @arg other: The other vector (L{Vector3d}).
155 @return: C{True} if so, C{False} otherwise.
157 @raise TypeError: Incompatible B{C{other}} C{type}.
158 '''
159 return self.length >= self.others(other).length
161# def __getitem__(self, key):
162# '''Return C{item} at index or slice C{[B{key}]}.
163# '''
164# return self.xyz[key]
166 def __gt__(self, other):
167 '''Is this vector longer than an other vector?
169 @arg other: The other vector (L{Vector3d}).
171 @return: C{True} if so, C{False} otherwise.
173 @raise TypeError: Incompatible B{C{other}} C{type}.
174 '''
175 return self.length > self.others(other).length
177 def __hash__(self): # PYCHOK no cover
178 '''Return this instance' C{hash}.
179 '''
180 return hash(self.xyz) # XXX id(self)?
182 def __iadd__(self, other):
183 '''Add this and an other vector I{in-place}, C{this += B{other}}.
185 @arg other: The other vector (L{Vector3d}).
187 @raise TypeError: Incompatible B{C{other}} C{type}.
188 '''
189 return self._xyz(self.plus(other))
191 def __ifloordiv__(self, other): # PYCHOK no cover
192 '''Not implemented.'''
193 return _NotImplemented(self, other)
195 def __imatmul__(self, other): # PYCHOK Python 3.5+
196 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
198 @arg other: The other vector (L{Vector3d}).
200 @raise TypeError: Incompatible B{C{other}} C{type}.
202 @see: Luciano Ramalho, "Fluent Python", page 397-398, O'Reilly 2016.
203 '''
204 return self._xyz(self.cross(other))
206 def __imod__(self, other): # PYCHOK no cover
207 '''Not implemented.'''
208 return _NotImplemented(self, other)
210 def __imul__(self, scalar):
211 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
213 @arg scalar: Factor (C{scalar}).
215 @raise TypeError: Non-scalar B{C{scalar}}.
216 '''
217 return self._xyz(self.times(scalar))
219 def __int__(self): # PYCHOK no cover
220 '''Not implemented.'''
221 return _NotImplemented(self)
223 def __ipow__(self, other, *mod): # PYCHOK no cover
224 '''Not implemented.'''
225 return _NotImplemented(self, other, *mod)
227 def __isub__(self, other):
228 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
230 @arg other: The other vector (L{Vector3d}).
232 @raise TypeError: Incompatible B{C{other}} C{type}.
233 '''
234 return self._xyz(self.minus(other))
236# def __iter__(self):
237# '''Return an C{iter}ator over this vector's components.
238# '''
239# return iter(self.xyz)
241 def __itruediv__(self, scalar):
242 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
244 @arg scalar: The divisor (C{scalar}).
246 @raise TypeError: Non-scalar B{C{scalar}}.
247 '''
248 return self._xyz(self.dividedBy(scalar))
250 def __le__(self, other): # Python 3+
251 '''Is this vector shorter than or equal to an other vector?
253 @arg other: The other vector (L{Vector3d}).
255 @return: C{True} if so, C{False} otherwise.
257 @raise TypeError: Incompatible B{C{other}} C{type}.
258 '''
259 return self.length <= self.others(other).length
261# def __len__(self):
262# '''Return C{3}, always.
263# '''
264# return len(self.xyz)
266 def __lt__(self, other): # Python 3+
267 '''Is this vector shorter than an other vector?
269 @arg other: The other vector (L{Vector3d}).
271 @return: C{True} if so, C{False} otherwise.
273 @raise TypeError: Incompatible B{C{other}} C{type}.
274 '''
275 return self.length < self.others(other).length
277 def __matmul__(self, other): # PYCHOK Python 3.5+
278 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
280 @arg other: The other vector (L{Vector3d}).
282 @return: Cross product (L{Vector3d}).
284 @raise TypeError: Incompatible B{C{other}} C{type}.
285 '''
286 return self.cross(other)
288 def __mod__(self, other): # PYCHOK no cover
289 '''Not implemented.'''
290 return _NotImplemented(self, other)
292 def __mul__(self, scalar):
293 '''Multiply this vector by a scalar, C{this * B{scalar}}.
295 @arg scalar: Factor (C{scalar}).
297 @return: Product (L{Vector3d}).
298 '''
299 return self.times(scalar)
301 def __ne__(self, other):
302 '''Is this vector not equal to an other vector?
304 @arg other: The other vector (L{Vector3d}).
306 @return: C{True} if so, C{False} otherwise.
308 @raise TypeError: Incompatible B{C{other}} C{type}.
309 '''
310 return not self.isequalTo(other, eps=EPS0)
312 def __neg__(self):
313 '''Return the opposite of this vector.
315 @return: This instance negated (L{Vector3d})
316 '''
317 return self.classof(-self.x, -self.y, -self.z)
319 def __pos__(self): # PYCHOK no cover
320 '''Return this vector I{as-is} or a copy.
322 @return: This instance (L{Vector3d})
323 '''
324 return self if _pos_self else self.copy()
326 def __pow__(self, other, *mod): # PYCHOK no cover
327 '''Not implemented.'''
328 return _NotImplemented(self, other, *mod)
330 __radd__ = __add__ # PYCHOK no cover
332 def __rdivmod__ (self, other): # PYCHOK no cover
333 '''Not implemented.'''
334 return _NotImplemented(self, other)
336# def __repr__(self):
337# '''Return the default C{repr(this)}.
338# '''
339# return self.toRepr()
341 def __rfloordiv__(self, other): # PYCHOK no cover
342 '''Not implemented.'''
343 return _NotImplemented(self, other)
345 def __rmatmul__(self, other): # PYCHOK Python 3.5+
346 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
348 @arg other: The other vector (L{Vector3d}).
350 @return: Cross product (L{Vector3d}).
352 @raise TypeError: Incompatible B{C{other}} C{type}.
353 '''
354 return self.others(other).cross(self)
356 def __rmod__(self, other): # PYCHOK no cover
357 '''Not implemented.'''
358 return _NotImplemented(self, other)
360 __rmul__ = __mul__
362 def __round__(self, ndigits=None): # PYCHOK no cover
363 '''Not implemented.'''
364 return _NotImplemented(self, ndigits=ndigits)
366 def __rpow__(self, other, *mod): # PYCHOK no cover
367 '''Not implemented.'''
368 return _NotImplemented(self, other, *mod)
370 def __rsub__(self, other): # PYCHOK no cover
371 '''Subtract this vector from an other vector, C{B{other} - this}.
373 @arg other: The other vector (L{Vector3d}).
375 @return: Difference (L{Vector3d}).
377 @raise TypeError: Incompatible B{C{other}} C{type}.
378 '''
379 return self.others(other).minus(self)
381 def __rtruediv__(self, scalar): # PYCHOK no cover
382 '''Not implemented.'''
383 return _NotImplemented(self, scalar)
385# def __str__(self):
386# '''Return the default C{str(self)}.
387# '''
388# return self.toStr()
390 def __sub__(self, other):
391 '''Subtract an other vector from this vector, C{this - B{other}}.
393 @arg other: The other vector (L{Vector3d}).
395 @return: Difference (L{Vector3d}).
397 @raise TypeError: Incompatible B{C{other}} C{type}.
398 '''
399 return self.minus(other)
401 def __truediv__(self, scalar):
402 '''Divide this vector by a scalar, C{this / B{scalar}}.
404 @arg scalar: The divisor (C{scalar}).
406 @return: Quotient (L{Vector3d}).
408 @raise TypeError: Non-scalar B{C{scalar}}.
409 '''
410 return self.dividedBy(scalar)
412 __trunc__ = __int__
414 if _sys_version_info2 < (3, 0): # PYCHOK no cover
415 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
416 __div__ = __truediv__
417 __idiv__ = __itruediv__
418 __long__ = __int__
419 __nonzero__ = __bool__
420 __rdiv__ = __rtruediv__
422 def angleTo(self, other, vSign=None, wrap=False):
423 '''Compute the angle between this and an other vector.
425 @arg other: The other vector (L{Vector3d}).
426 @kwarg vSign: Optional vector, if supplied (and out of the
427 plane of this and the other), angle is signed
428 positive if this->other is clockwise looking
429 along vSign or negative in opposite direction,
430 otherwise angle is unsigned.
431 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
433 @return: Angle (C{radians}).
435 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
436 '''
437 x = self.cross(other)
438 s = x.length
439 # use vSign as reference to set sign of s
440 if s and vSign and x.dot(vSign) < 0:
441 s = -s
443 a = atan2(s, self.dot(other))
444 if wrap and fabs(a) > PI:
445 a -= copysign0(PI2, a)
446 return a
448 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
449 '''Apply a 2-argument function pairwise to the components
450 of this and an other vector.
452 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
453 return a C{scalar} or L{INT0} result.
454 @arg other_x: Other X component (C{scalar}) or a vector
455 with X, Y and Z components (C{Cartesian},
456 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
457 L{Vector3Tuple} or L{Vector4Tuple}).
458 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
459 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
461 @return: New, applied vector (L{Vector3d}).
463 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
464 '''
465 if not callable(fun2):
466 raise _IsnotError(callable.__name__, fun2=fun2)
468 if fun2_kwds:
469 def _f2(a, b):
470 return fun2(a, b, **fun2_kwds)
471 else:
472 _f2 = fun2
474 xyz = _other_x_y_z3(other_x, y_z)
475 xyz = (_f2(a, b) for a, b in _zip(self.xyz, xyz)) # strict=True
476 return self.classof(*xyz)
478 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
479 '''Compute the cross product of this and an other vector.
481 @arg other: The other vector (L{Vector3d}).
482 @kwarg raiser: Optional, L{CrossError} label if raised (C{str},
483 non-L{NN}).
484 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as
485 C{x}, C{y}, and C{z}.
487 @return: Cross product (L{Vector3d}).
489 @raise CrossError: Zero or near-zero cross product and both
490 B{C{raiser}} and L{pygeodesy.crosserrors} set.
492 @raise TypeError: Incompatible B{C{other}} C{type}.
493 '''
494 other = self.others(other)
496 x = self.y * other.z - self.z * other.y
497 y = self.z * other.x - self.x * other.z
498 z = self.x * other.y - self.y * other.x
500 if raiser and self.crosserrors and eps0 > 0 \
501 and max(map1(fabs, x, y, z)) < eps0:
502 t = self.isequalTo(other, eps=eps0)
503 s = self._fromll or self
504 r = other._fromll or other
505 t = _coincident_ if t else _colinear_
506 raise CrossError(raiser, s, other=r, txt=t)
508 return self.classof(x, y, z)
510 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
511 def crosserrors(self):
512 '''Get L{CrossError} exceptions (C{bool}).
513 '''
514 return self._crosserrors
516 @crosserrors.setter # PYCHOK setter!
517 def crosserrors(self, raiser):
518 '''Raise L{CrossError} exceptions (C{bool}).
519 '''
520 self._crosserrors = bool(raiser)
522 def dividedBy(self, divisor):
523 '''Divide this vector by a scalar.
525 @arg divisor: The divisor (C{scalar}).
527 @return: New, scaled vector (L{Vector3d}).
529 @raise TypeError: Non-scalar B{C{divisor}}.
531 @raise VectorError: Invalid or zero B{C{divisor}}.
532 '''
533 d = Scalar(divisor=divisor)
534 try:
535 return self._times(_1_0 / d)
536 except (ValueError, ZeroDivisionError) as x:
537 raise VectorError(divisor=divisor, cause=x)
539 def dot(self, other):
540 '''Compute the dot (scalar) product of this and an other vector.
542 @arg other: The other vector (L{Vector3d}).
544 @return: Dot product (C{float}).
546 @raise TypeError: Incompatible B{C{other}} C{type}.
547 '''
548 return self.length2 if other is self else \
549 fdot(self.xyz, *self.others(other).xyz)
551 @deprecated_method
552 def equals(self, other, units=False): # PYCHOK no cover
553 '''DEPRECATED, use method C{isequalTo}.
554 '''
555 return self.isequalTo(other, units=units)
557 @Property_RO
558 def euclid(self):
559 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
561 @see: Properties C{length} and C{length2} and function
562 L{pygeodesy.euclid_}.
563 '''
564 return Float(euclid=euclid_(self.x, self.y, self.z))
566 def equirectangular(self, other):
567 '''I{Approximate} the different between this and an other vector.
569 @arg other: Vector to subtract (C{Vector3dBase}).
571 @return: The lenght I{squared} of the difference (C{Float}).
573 @raise TypeError: Incompatible B{C{other}} C{type}.
575 @see: Property C{length2}.
576 '''
577 d = self.minus(other)
578 return Float(equirectangular=hypot2_(d.x, d.y, d.z))
580 @Property
581 def _fromll(self):
582 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
583 '''
584 return self._ll
586 @_fromll.setter # PYCHOK setter!
587 def _fromll(self, ll):
588 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
589 '''
590 self._ll = ll or None
592 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
593 '''Locate the vector at a given fraction between (or along) this
594 and an other vector.
596 @arg other: The other vector (L{Vector3d}).
597 @arg fraction: Fraction between both vectors (C{scalar},
598 0.0 for this and 1.0 for the other vector).
600 @return: Intermediate vector (L{Vector3d}).
602 @raise TypeError: Incompatible B{C{other}} C{type}.
603 '''
604 f = Scalar(fraction=fraction)
605 if isnear0(f): # PYCHOK no cover
606 r = self
607 else:
608 r = self.others(other)
609 if not isnear1(f): # self * (1 - f) + r * f
610 r = self.plus(r.minus(self)._times(f))
611 return r
613 def isconjugateTo(self, other, minum=1, eps=EPS):
614 '''Determine whether this and an other vector are conjugates.
616 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
617 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
618 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
619 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
620 same units as C{x}, C{y}, and C{z}.
622 @return: C{True} if both vector's components either match
623 or at least C{B{minum}} have opposite signs.
625 @raise TypeError: Incompatible B{C{other}} C{type}.
627 @see: Method C{isequalTo}.
628 '''
629 self.others(other)
630 n = 0
631 for a, b in zip(self.xyz, other.xyz):
632 if fabs(a + b) < eps and ((a < 0 and b > 0) or
633 (a > 0 and b < 0)):
634 n += 1 # conjugate
635 elif fabs(a - b) > eps:
636 return False # unequal
637 return bool(n >= minum)
639 def isequalTo(self, other, units=False, eps=EPS):
640 '''Check if this and an other vector are equal or equivalent.
642 @arg other: The other vector (L{Vector3d}).
643 @kwarg units: Optionally, compare the normalized, unit
644 version of both vectors.
645 @kwarg eps: Tolerance for equality (C{scalar}), same units as
646 C{x}, C{y}, and C{z}.
648 @return: C{True} if vectors are identical, C{False} otherwise.
650 @raise TypeError: Incompatible B{C{other}} C{type}.
652 @see: Method C{isconjugateTo}.
653 '''
654 if units:
655 self.others(other)
656 d = self.unit().minus(other.unit())
657 else:
658 d = self.minus(other)
659 return max(map(fabs, d.xyz)) < eps
661 @Property_RO
662 def length(self): # __dict__ value overwritten by Property_RO C{_united}
663 '''Get the length (norm, magnitude) of this vector (C{Float}).
665 @see: Properties L{length2} and L{euclid}.
666 '''
667 return Float(length=hypot_(self.x, self.y, self.z))
669 @Property_RO
670 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
671 '''Get the length I{squared} of this vector (C{Float}).
673 @see: Property L{length} and method C{equirectangular}.
674 '''
675 return Float(length2=hypot2_(self.x, self.y, self.z))
677 def minus(self, other):
678 '''Subtract an other vector from this vector.
680 @arg other: The other vector (L{Vector3d}).
682 @return: New vector difference (L{Vector3d}).
684 @raise TypeError: Incompatible B{C{other}} C{type}.
685 '''
686 self.others(other)
687 return self._minus(other.x, other.y, other.z)
689 def _minus(self, x, y, z):
690 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
691 '''
692 return self.classof(self.x - x, self.y - y, self.z - z)
694 def minus_(self, other_x, *y_z):
695 '''Subtract separate X, Y and Z components from this vector.
697 @arg other_x: X component (C{scalar}) or a vector's
698 X, Y, and Z components (C{Cartesian},
699 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
700 L{Vector3Tuple}, L{Vector4Tuple}).
701 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
702 ignored if B{C{other_x}} is not C{scalar}.
704 @return: New, vectiorial vector (L{Vector3d}).
706 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
707 '''
708 return self._minus(*_other_x_y_z3(other_x, y_z))
710 def negate(self):
711 '''Return this vector in opposite direction.
713 @return: New, opposite vector (L{Vector3d}).
714 '''
715 return self.classof(-self.x, -self.y, -self.z)
717 __neg__ = negate # PYCHOK no cover
719 @Property_RO
720 def _N_vector(self):
721 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
722 '''
723 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name)
725 def others(self, *other, **name_other_up):
726 '''Refined class comparison.
728 @arg other: The other vector (L{Vector3d}).
729 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
730 keyword arguments.
732 @return: The B{C{other}} if compatible.
734 @raise TypeError: Incompatible B{C{other}} C{type}.
735 '''
736 other, name, up = _xother3(self, other, **name_other_up)
737 if not isinstance(other, Vector3dBase):
738 _NamedBase.others(self, other, name=name, up=up + 1)
739 return other
741 def plus(self, other):
742 '''Add this vector and an other vector.
744 @arg other: The other vector (L{Vector3d}).
746 @return: Vectorial sum (L{Vector3d}).
748 @raise TypeError: Incompatible B{C{other}} C{type}.
749 '''
750 self.others(other)
751 return self._plus(other.x, other.y, other.z)
753 sum = plus # alternate name
755 def _plus(self, x, y, z):
756 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
757 '''
758 return self.classof(self.x + x, self.y + y, self.z + z)
760 def plus_(self, other_x, *y_z):
761 '''Sum of this vector and separate X, Y and Z components.
763 @arg other_x: X component (C{scalar}) or a vector's
764 X, Y, and Z components (C{Cartesian},
765 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
766 L{Vector3Tuple}, L{Vector4Tuple}).
767 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
768 ignored if B{C{other_x}} is not C{scalar}.
770 @return: New, vectiorial vector (L{Vector3d}).
772 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
773 '''
774 return self._plus(*_other_x_y_z3(other_x, y_z))
776 def rotate(self, axis, theta):
777 '''Rotate this vector around an axis by a specified angle.
779 @arg axis: The axis being rotated around (L{Vector3d}).
780 @arg theta: The angle of rotation (C{radians}).
782 @return: New, rotated vector (L{Vector3d}).
784 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
785 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
786 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
787 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
788 '''
789 s, c = sincos2(theta) # rotation angle
790 d = _1_0 - c
791 if d or s:
792 p = self.unit().xyz # point being rotated
793 r = self.others(axis=axis).unit() # axis being rotated around
795 ax, ay, az = r.xyz # quaternion-derived rotation matrix
796 bx, by, bz = r.times(d).xyz
797 sx, sy, sz = r.times(s).xyz
799 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)
800 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)
801 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)
802 else: # unrotated
803 x, y, z = self.xyz
804 return self.classof(x, y, z)
806 @deprecated_method
807 def rotateAround(self, axis, theta): # PYCHOK no cover
808 '''DEPRECATED, use method C{rotate}.'''
809 return self.rotate(axis, theta)
811 def times(self, factor):
812 '''Multiply this vector by a scalar.
814 @arg factor: Scale factor (C{scalar}).
816 @return: New, scaled vector (L{Vector3d}).
818 @raise TypeError: Non-scalar B{C{factor}}.
819 '''
820 return self._times(Scalar(factor=factor))
822 def _times(self, s):
823 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
824 '''
825 return self.classof(self.x * s, self.y * s, self.z * s)
827 def times_(self, other_x, *y_z):
828 '''Multiply this vector's components by separate X, Y and Z factors.
830 @arg other_x: X scale factor (C{scalar}) or a vector's
831 X, Y, and Z components as scale factors
832 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
833 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
834 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
835 ignored if B{C{other_x}} is not C{scalar}.
837 @return: New, scaled vector (L{Vector3d}).
839 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
840 '''
841 x, y, z = _other_x_y_z3(other_x, y_z)
842 return self.classof(self.x * x, self.y * y, self.z * z)
844# @deprecated_method
845# def to2ab(self): # PYCHOK no cover
846# '''DEPRECATED, use property C{Nvector.philam}.
847#
848# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
849# '''
850# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)
852# @deprecated_method
853# def to2ll(self): # PYCHOK no cover
854# '''DEPRECATED, use property C{Nvector.latlon}.
855#
856# @return: A L{LatLon2Tuple}C{(lat, lon)}.
857# '''
858# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)
860 @deprecated_method
861 def to3xyz(self): # PYCHOK no cover
862 '''DEPRECATED, use property L{xyz}.
863 '''
864 return self.xyz
866 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
867 '''Return a string representation of this vector.
869 @kwarg prec: Number of decimal places (C{int}).
870 @kwarg fmt: Enclosing format to use (C{str}).
871 @kwarg sep: Separator between components (C{str}).
873 @return: Vector as "(x, y, z)" (C{str}).
874 '''
875 t = sep.join(strs(self.xyz, prec=prec))
876 return (fmt % (t,)) if fmt else t
878 def unit(self, ll=None):
879 '''Normalize this vector to unit length.
881 @kwarg ll: Optional, original location (C{LatLon}).
883 @return: Normalized vector (L{Vector3d}).
884 '''
885 u = self._united
886 if ll:
887 u._fromll = ll
888 return u
890 @Property_RO
891 def _united(self): # __dict__ value overwritten below
892 '''(INTERNAL) Get normalized vector (L{Vector3d}).
893 '''
894 n = self.length
895 if n > EPS0 and fabs(n - _1_0) > EPS0:
896 u = self._xnamed(self.dividedBy(n))
897 u._update(False, length=_1_0, length2=_1_0, _united=u)
898 else:
899 u = self.copy()
900 u._update(False, _united=u)
901 if self._fromll:
902 u._fromll = self._fromll
903 return u
905 @Property
906 def x(self):
907 '''Get the X component (C{float}).
908 '''
909 return self._x
911 @x.setter # PYCHOK setter!
912 def x(self, x):
913 '''Set the X component, if different (C{float}).
914 '''
915 x = Float(x=x)
916 if self._x != x:
917 _update_all(self)
918 self._x = x
920 @Property
921 def xyz(self):
922 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
923 '''
924 return _MODS.namedTuples.Vector3Tuple(self.x, self.y, self.z, name=self.name)
926 @xyz.setter # PYCHOK setter!
927 def xyz(self, xyz):
928 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
929 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
930 or a C{tuple} or C{list} of 3+ C{scalar} values).
931 '''
932 if islistuple(xyz, 3):
933 self._xyz(*xyz[:3])
934 else:
935 self._xyz(xyz)
937 def _xyz(self, x_xyz, *y_z):
938 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
939 '''
940 if len(self.__dict__) > 3: # any other than initial ._x, ._y and ._z attrs
941 _update_all(self)
942 try:
943 self._x, \
944 self._y, \
945 self._z = map1(_float0, x_xyz, *y_z) if y_z else x_xyz.xyz
946 except (AttributeError, TypeError, ValueError) as x:
947 raise VectorError(cause=x, **_xyzkwds(y_z, x_xyz=x_xyz))
948 return self
950 @Property_RO
951 def x2y2z2(self):
952 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
953 '''
954 return self.x**2, self.y**2, self.z**2
956 @Property
957 def y(self):
958 '''Get the Y component (C{float}).
959 '''
960 return self._y
962 @y.setter # PYCHOK setter!
963 def y(self, y):
964 '''Set the Y component, if different (C{float}).
965 '''
966 y = Float(y=y)
967 if self._y != y:
968 _update_all(self)
969 self._y = y
971 @Property
972 def z(self):
973 '''Get the Z component (C{float}).
974 '''
975 return self._z
977 @z.setter # PYCHOK setter!
978 def z(self, z):
979 '''Set the Z component, if different (C{float}).
980 '''
981 z = Float(z=z)
982 if self._z != z:
983 _update_all(self)
984 self._z = z
987def _other_x_y_z3(other_x, y_z):
988 '''(INTERNAL) Helper for C{Vector3dBase.apply} and C{Vector3dBase.times_}.
989 '''
990 try:
991 return map1(_float0, other_x, *y_z) if y_z else \
992 (other_x.x, other_x.y, other_x.z) # not .xyz!
993 except (AttributeError, TypeError, ValueError) as x:
994 raise _InvalidError(cause=x, **_xyzkwds(y_z, other_x=other_x))
997def _xyzkwds(y_z, **xyz): # PYCHOK no cover
998 '''(INTERANL) Helper for C{_other_x_y_z3} and C{Vector3dBase._xyz}.
999 '''
1000 if y_z:
1001 d = dict(_zip((_y_, _z_), y_z)) # if y_z else {}, strict=True
1002 for x in xyz.values():
1003 d.update(x=x)
1004 return d
1005 return xyz
1007# **) MIT License
1008#
1009# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
1010#
1011# Permission is hereby granted, free of charge, to any person obtaining a
1012# copy of this software and associated documentation files (the "Software"),
1013# to deal in the Software without restriction, including without limitation
1014# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1015# and/or sell copies of the Software, and to permit persons to whom the
1016# Software is furnished to do so, subject to the following conditions:
1017#
1018# The above copyright notice and this permission notice shall be included
1019# in all copies or substantial portions of the Software.
1020#
1021# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1022# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1023# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1024# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1025# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1026# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1027# OTHER DEALINGS IN THE SOFTWARE.