Coverage for pygeodesy/vector3dBase.py: 96%
274 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-05-20 11:54 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-05-20 11:54 -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, _1_0
14from pygeodesy.errors import CrossError, _InvalidError, _IsnotError, VectorError
15from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_
16from pygeodesy.fsums import fsum1f_, _pos_self
17from pygeodesy.interns import NN, _coincident_, _colinear_, _COMMASPACE_, _y_, _z_
18from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _sys, _sys_version_info2
19from pygeodesy.named import _NamedBase, _NotImplemented, _xother3
20from pygeodesy.namedTuples import Vector3Tuple
21from pygeodesy.props import deprecated_method, Property, Property_RO, \
22 property_doc_, _update_all
23from pygeodesy.streprs import Fmt, strs
24from pygeodesy.units import Float, Scalar
25from pygeodesy.utily import atan2, fabs, sincos2
27# from math import atan2, fabs # from .utily
29__all__ = _ALL_LAZY.vector3dBase
30__version__ = '23.05.15'
33class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
34 '''(INTERNAL) Generic 3-D vector base class.
36 In a geodesy context, these may be used to represent:
37 - n-vector representing a normal to point on earth's surface
38 - earth-centered, earth-fixed cartesian (= spherical n-vector)
39 - great circle normal to vector
40 - motion vector on earth's surface
41 - etc.
42 '''
43 _crosserrors = True # un/set by .errors.crosserrors
45 _ll = None # original latlon, '_fromll'
46 _x = INT0 # X component
47 _y = INT0 # Y component
48 _z = INT0 # Z component
50 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, name=NN):
51 '''New L{Vector3d} or C{Vector3dBase} instance.
53 The vector may be normalised or use x, y, z for position and
54 distance from earth centre or height relative to the surface
55 of the earth' sphere or ellipsoid.
57 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
58 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
59 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
60 C{list} of 3+ C{scalar} values).
61 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}
62 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
63 @kwarg z: Z 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 ll: Optional latlon reference (C{LatLon}).
66 @kwarg name: Optional name (C{str}).
68 @raise VectorError: Invalid B{C{x_xyz}}.
69 '''
70 if isscalar(x_xyz):
71 self._xyz(x_xyz, y, z)
72 else:
73 self.xyz = x_xyz
74 if ll:
75 self._ll = ll
76 if name:
77 self.name = name
79 def __abs__(self):
80 '''Return the norm of this vector.
82 @return: Norm, unit length (C{float});
83 '''
84 return self.length
86 def __add__(self, other):
87 '''Add this to an other vector (L{Vector3d}).
89 @return: Vectorial sum (L{Vector3d}).
91 @raise TypeError: Incompatible B{C{other}} C{type}.
92 '''
93 return self.plus(other)
95 def __bool__(self): # PYCHOK PyChecker
96 '''Is this vector non-zero?
97 '''
98 return bool(self.x or self.y or self.z)
100 def __ceil__(self): # PYCHOK no cover
101 '''Not implemented.'''
102 return _NotImplemented(self)
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 '''Not implemented.'''
138 return _NotImplemented(self)
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.others(other).length
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.others(other).length
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", page 397-398, O'Reilly 2016.
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 '''Not implemented.'''
219 return _NotImplemented(self)
221 def __ipow__(self, other, *mod): # PYCHOK no cover
222 '''Not implemented.'''
223 return _NotImplemented(self, other, *mod)
225 def __isub__(self, other):
226 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
228 @arg other: The other vector (L{Vector3d}).
230 @raise TypeError: Incompatible B{C{other}} C{type}.
231 '''
232 return self._xyz(self.minus(other))
234# def __iter__(self):
235# '''Return an C{iter}ator over this vector's components.
236# '''
237# return iter(self.xyz)
239 def __itruediv__(self, scalar):
240 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
242 @arg scalar: The divisor (C{scalar}).
244 @raise TypeError: Non-scalar B{C{scalar}}.
245 '''
246 return self._xyz(self.dividedBy(scalar))
248 def __le__(self, other): # Python 3+
249 '''Is this vector shorter than or equal to an other vector?
251 @arg other: The other vector (L{Vector3d}).
253 @return: C{True} if so, C{False} otherwise.
255 @raise TypeError: Incompatible B{C{other}} C{type}.
256 '''
257 return self.length <= self.others(other).length
259# def __len__(self):
260# '''Return C{3}, always.
261# '''
262# return len(self.xyz)
264 def __lt__(self, other): # Python 3+
265 '''Is this vector shorter than an other vector?
267 @arg other: The other vector (L{Vector3d}).
269 @return: C{True} if so, C{False} otherwise.
271 @raise TypeError: Incompatible B{C{other}} C{type}.
272 '''
273 return self.length < self.others(other).length
275 def __matmul__(self, other): # PYCHOK Python 3.5+
276 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
278 @arg other: The other vector (L{Vector3d}).
280 @return: Cross product (L{Vector3d}).
282 @raise TypeError: Incompatible B{C{other}} C{type}.
283 '''
284 return self.cross(other)
286 def __mod__(self, other): # PYCHOK no cover
287 '''Not implemented.'''
288 return _NotImplemented(self, other)
290 def __mul__(self, scalar):
291 '''Multiply this vector by a scalar, C{this * B{scalar}}.
293 @arg scalar: Factor (C{scalar}).
295 @return: Product (L{Vector3d}).
296 '''
297 return self.times(scalar)
299 def __ne__(self, other):
300 '''Is this vector not equal to an other vector?
302 @arg other: The other vector (L{Vector3d}).
304 @return: C{True} if so, C{False} otherwise.
306 @raise TypeError: Incompatible B{C{other}} C{type}.
307 '''
308 return not self.isequalTo(other, eps=EPS0)
310 def __neg__(self):
311 '''Return the opposite of this vector.
313 @return: This instance negated (L{Vector3d})
314 '''
315 return self.classof(-self.x, -self.y, -self.z)
317 def __pos__(self): # PYCHOK no cover
318 '''Return this vector I{as-is} or a copy.
320 @return: This instance (L{Vector3d})
321 '''
322 return self if _pos_self else self.copy()
324 def __pow__(self, other, *mod): # PYCHOK no cover
325 '''Not implemented.'''
326 return _NotImplemented(self, other, *mod)
328 __radd__ = __add__ # PYCHOK no cover
330 def __rdivmod__ (self, other): # PYCHOK no cover
331 '''Not implemented.'''
332 return _NotImplemented(self, other)
334# def __repr__(self):
335# '''Return the default C{repr(this)}.
336# '''
337# return self.toRepr()
339 def __rfloordiv__(self, other): # PYCHOK no cover
340 '''Not implemented.'''
341 return _NotImplemented(self, other)
343 def __rmatmul__(self, other): # PYCHOK Python 3.5+
344 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
346 @arg other: The other vector (L{Vector3d}).
348 @return: Cross product (L{Vector3d}).
350 @raise TypeError: Incompatible B{C{other}} C{type}.
351 '''
352 return self.others(other).cross(self)
354 def __rmod__(self, other): # PYCHOK no cover
355 '''Not implemented.'''
356 return _NotImplemented(self, other)
358 __rmul__ = __mul__
360 def __round__(self, ndigits=None): # PYCHOK no cover
361 '''Not implemented.'''
362 return _NotImplemented(self, ndigits=ndigits)
364 def __rpow__(self, other, *mod): # PYCHOK no cover
365 '''Not implemented.'''
366 return _NotImplemented(self, other, *mod)
368 def __rsub__(self, other): # PYCHOK no cover
369 '''Subtract this vector from an other vector, C{B{other} - this}.
371 @arg other: The other vector (L{Vector3d}).
373 @return: Difference (L{Vector3d}).
375 @raise TypeError: Incompatible B{C{other}} C{type}.
376 '''
377 return self.others(other).minus(self)
379 def __rtruediv__(self, scalar): # PYCHOK no cover
380 '''Not implemented.'''
381 return _NotImplemented(self, scalar)
383 def __sizeof__(self): # PYCHOK not special in Python 2-
384 '''Return the current size of this vector in C{bytes}.
385 '''
386 # self._x, self._y, self._z, self._ll, ...
387 v = self.__dict__.values # avoid recursion
388 return sum(map1(_sys.getsizeof, (o for o in v() if o is not self)))
390# def __str__(self):
391# '''Return the default C{str(self)}.
392# '''
393# return self.toStr()
395 def __sub__(self, other):
396 '''Subtract an other vector from this vector, C{this - B{other}}.
398 @arg other: The other vector (L{Vector3d}).
400 @return: Difference (L{Vector3d}).
402 @raise TypeError: Incompatible B{C{other}} C{type}.
403 '''
404 return self.minus(other)
406 def __truediv__(self, scalar):
407 '''Divide this vector by a scalar, C{this / B{scalar}}.
409 @arg scalar: The divisor (C{scalar}).
411 @return: Quotient (L{Vector3d}).
413 @raise TypeError: Non-scalar B{C{scalar}}.
414 '''
415 return self.dividedBy(scalar)
417 __trunc__ = __int__
419 if _sys_version_info2 < (3, 0): # PYCHOK no cover
420 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
421 __div__ = __truediv__
422 __idiv__ = __itruediv__
423 __long__ = __int__
424 __nonzero__ = __bool__
425 __rdiv__ = __rtruediv__
427 def angleTo(self, other, vSign=None, wrap=False):
428 '''Compute the angle between this and an other vector.
430 @arg other: The other vector (L{Vector3d}).
431 @kwarg vSign: Optional vector, if supplied (and out of the
432 plane of this and the other), angle is signed
433 positive if this->other is clockwise looking
434 along vSign or negative in opposite direction,
435 otherwise angle is unsigned.
436 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
438 @return: Angle (C{radians}).
440 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
441 '''
442 x = self.cross(other)
443 s = x.length
444 # use vSign as reference to set sign of s
445 if s and vSign and x.dot(vSign) < 0:
446 s = -s
448 a = atan2(s, self.dot(other))
449 if wrap and fabs(a) > PI:
450 a -= copysign0(PI2, a)
451 return a
453 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
454 '''Apply a 2-argument function pairwise to the components
455 of this and an other vector.
457 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
458 return a C{scalar} or L{INT0} result.
459 @arg other_x: Other X component (C{scalar}) or a vector
460 with X, Y and Z components (C{Cartesian},
461 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
462 L{Vector3Tuple} or L{Vector4Tuple}).
463 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
464 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
466 @return: New, applied vector (L{Vector3d}).
468 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
469 '''
470 if not callable(fun2):
471 raise _IsnotError(callable.__name__, fun2=fun2)
473 if fun2_kwds:
474 def _f2(a, b):
475 return fun2(a, b, **fun2_kwds)
476 else:
477 _f2 = fun2
479 xyz = _other_x_y_z3(other_x, y_z)
480 xyz = (_f2(a, b) for a, b in _zip(self.xyz, xyz)) # strict=True
481 return self.classof(*xyz)
483 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
484 '''Compute the cross product of this and an other vector.
486 @arg other: The other vector (L{Vector3d}).
487 @kwarg raiser: Optional, L{CrossError} label if raised (C{str},
488 non-L{NN}).
489 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as
490 C{x}, C{y}, and C{z}.
492 @return: Cross product (L{Vector3d}).
494 @raise CrossError: Zero or near-zero cross product and both
495 B{C{raiser}} and L{pygeodesy.crosserrors} set.
497 @raise TypeError: Incompatible B{C{other}} C{type}.
498 '''
499 other = self.others(other)
501 x = fsum1f_(self.y * other.z, -self.z * other.y)
502 y = fsum1f_(self.z * other.x, -self.x * other.z)
503 z = fsum1f_(self.x * other.y, -self.y * other.x)
505 if raiser and self.crosserrors and eps0 > 0 \
506 and max(map1(fabs, x, y, z)) < eps0:
507 t = self.isequalTo(other, eps=eps0)
508 s = self._fromll or self
509 r = other._fromll or other
510 t = _coincident_ if t else _colinear_
511 raise CrossError(raiser, s, other=r, txt=t)
513 return self.classof(x, y, z)
515 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
516 def crosserrors(self):
517 '''Get L{CrossError} exceptions (C{bool}).
518 '''
519 return self._crosserrors
521 @crosserrors.setter # PYCHOK setter!
522 def crosserrors(self, raiser):
523 '''Raise L{CrossError} exceptions (C{bool}).
524 '''
525 self._crosserrors = bool(raiser)
527 def dividedBy(self, divisor):
528 '''Divide this vector by a scalar.
530 @arg divisor: The divisor (C{scalar}).
532 @return: New, scaled vector (L{Vector3d}).
534 @raise TypeError: Non-scalar B{C{divisor}}.
536 @raise VectorError: Invalid or zero B{C{divisor}}.
537 '''
538 d = Scalar(divisor=divisor)
539 try:
540 return self._times(_1_0 / d)
541 except (ValueError, ZeroDivisionError) as x:
542 raise VectorError(divisor=divisor, cause=x)
544 def dot(self, other):
545 '''Compute the dot (scalar) product of this and an other vector.
547 @arg other: The other vector (L{Vector3d}).
549 @return: Dot product (C{float}).
551 @raise TypeError: Incompatible B{C{other}} C{type}.
552 '''
553 return self.length2 if other is self else \
554 fdot(self.xyz, *self.others(other).xyz)
556 @deprecated_method
557 def equals(self, other, units=False): # PYCHOK no cover
558 '''DEPRECATED, use method C{isequalTo}.
559 '''
560 return self.isequalTo(other, units=units)
562 @Property_RO
563 def euclid(self):
564 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
566 @see: Properties C{length} and C{length2} and function
567 L{pygeodesy.euclid_}.
568 '''
569 return Float(euclid=euclid_(self.x, self.y, self.z))
571 def equirectangular(self, other):
572 '''I{Approximate} the different between this and an other vector.
574 @arg other: Vector to subtract (C{Vector3dBase}).
576 @return: The lenght I{squared} of the difference (C{Float}).
578 @raise TypeError: Incompatible B{C{other}} C{type}.
580 @see: Property C{length2}.
581 '''
582 d = self.minus(other)
583 return Float(equirectangular=hypot2_(d.x, d.y, d.z))
585 @Property
586 def _fromll(self):
587 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
588 '''
589 return self._ll
591 @_fromll.setter # PYCHOK setter!
592 def _fromll(self, ll):
593 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
594 '''
595 self._ll = ll or None
597 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
598 '''Locate the vector at a given fraction between (or along) this
599 and an other vector.
601 @arg other: The other vector (L{Vector3d}).
602 @arg fraction: Fraction between both vectors (C{scalar},
603 0.0 for this and 1.0 for the other vector).
605 @return: Intermediate vector (L{Vector3d}).
607 @raise TypeError: Incompatible B{C{other}} C{type}.
608 '''
609 f = Scalar(fraction=fraction)
610 if isnear0(f): # PYCHOK no cover
611 r = self
612 else:
613 r = self.others(other)
614 if not isnear1(f): # self * (1 - f) + r * f
615 r = self.plus(r.minus(self)._times(f))
616 return r
618 def isconjugateTo(self, other, minum=1, eps=EPS):
619 '''Determine whether this and an other vector are conjugates.
621 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
622 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
623 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
624 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
625 same units as C{x}, C{y}, and C{z}.
627 @return: C{True} if both vector's components either match
628 or at least C{B{minum}} have opposite signs.
630 @raise TypeError: Incompatible B{C{other}} C{type}.
632 @see: Method C{isequalTo}.
633 '''
634 self.others(other)
635 n = 0
636 for a, b in zip(self.xyz, other.xyz):
637 if fabs(a + b) < eps and ((a < 0 and b > 0) or
638 (a > 0 and b < 0)):
639 n += 1 # conjugate
640 elif fabs(a - b) > eps:
641 return False # unequal
642 return bool(n >= minum)
644 def isequalTo(self, other, units=False, eps=EPS):
645 '''Check if this and an other vector are equal or equivalent.
647 @arg other: The other vector (L{Vector3d}).
648 @kwarg units: Optionally, compare the normalized, unit
649 version of both vectors.
650 @kwarg eps: Tolerance for equality (C{scalar}), same units as
651 C{x}, C{y}, and C{z}.
653 @return: C{True} if vectors are identical, C{False} otherwise.
655 @raise TypeError: Incompatible B{C{other}} C{type}.
657 @see: Method C{isconjugateTo}.
658 '''
659 if units:
660 self.others(other)
661 d = self.unit().minus(other.unit())
662 else:
663 d = self.minus(other)
664 return max(map(fabs, d.xyz)) < eps
666 @Property_RO
667 def length(self): # __dict__ value overwritten by Property_RO C{_united}
668 '''Get the length (norm, magnitude) of this vector (C{Float}).
670 @see: Properties L{length2} and L{euclid}.
671 '''
672 return Float(length=hypot_(self.x, self.y, self.z))
674 @Property_RO
675 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
676 '''Get the length I{squared} of this vector (C{Float}).
678 @see: Property L{length} and method C{equirectangular}.
679 '''
680 return Float(length2=hypot2_(self.x, self.y, self.z))
682 def minus(self, other):
683 '''Subtract an other vector from this vector.
685 @arg other: The other vector (L{Vector3d}).
687 @return: New vector difference (L{Vector3d}).
689 @raise TypeError: Incompatible B{C{other}} C{type}.
690 '''
691 self.others(other)
692 return self._minus(other.x, other.y, other.z)
694 def _minus(self, x, y, z):
695 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
696 '''
697 return self.classof(self.x - x, self.y - y, self.z - z)
699 def minus_(self, other_x, *y_z):
700 '''Subtract separate X, Y and Z components from this vector.
702 @arg other_x: X component (C{scalar}) or a vector's
703 X, Y, and Z components (C{Cartesian},
704 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
705 L{Vector3Tuple}, L{Vector4Tuple}).
706 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
707 ignored if B{C{other_x}} is not C{scalar}.
709 @return: New, vectiorial vector (L{Vector3d}).
711 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
712 '''
713 return self._minus(*_other_x_y_z3(other_x, y_z))
715 def negate(self):
716 '''Return this vector in opposite direction.
718 @return: New, opposite vector (L{Vector3d}).
719 '''
720 return self.classof(-self.x, -self.y, -self.z)
722 __neg__ = negate # PYCHOK no cover
724 @Property_RO
725 def _N_vector(self):
726 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
727 '''
728 return _MODS.nvectorBase._N_vector_(*self.xyz, name=self.name)
730 def others(self, *other, **name_other_up):
731 '''Refined class comparison.
733 @arg other: The other vector (L{Vector3d}).
734 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
735 keyword arguments.
737 @return: The B{C{other}} if compatible.
739 @raise TypeError: Incompatible B{C{other}} C{type}.
740 '''
741 other, name, up = _xother3(self, other, **name_other_up)
742 if not isinstance(other, Vector3dBase):
743 _NamedBase.others(self, other, name=name, up=up + 1)
744 return other
746 def plus(self, other):
747 '''Add this vector and an other vector.
749 @arg other: The other vector (L{Vector3d}).
751 @return: Vectorial sum (L{Vector3d}).
753 @raise TypeError: Incompatible B{C{other}} C{type}.
754 '''
755 self.others(other)
756 return self._plus(other.x, other.y, other.z)
758 sum = plus # alternate name
760 def _plus(self, x, y, z):
761 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
762 '''
763 return self.classof(self.x + x, self.y + y, self.z + z)
765 def plus_(self, other_x, *y_z):
766 '''Sum of this vector and separate X, Y and Z components.
768 @arg other_x: X component (C{scalar}) or a vector's
769 X, Y, and Z components (C{Cartesian},
770 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
771 L{Vector3Tuple}, L{Vector4Tuple}).
772 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
773 ignored if B{C{other_x}} is not C{scalar}.
775 @return: New, vectiorial vector (L{Vector3d}).
777 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
778 '''
779 return self._plus(*_other_x_y_z3(other_x, y_z))
781 def rotate(self, axis, theta):
782 '''Rotate this vector around an axis by a specified angle.
784 @arg axis: The axis being rotated around (L{Vector3d}).
785 @arg theta: The angle of rotation (C{radians}).
787 @return: New, rotated vector (L{Vector3d}).
789 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
790 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
791 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
792 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
793 '''
794 s, c = sincos2(theta) # rotation angle
795 d = _1_0 - c
796 if d or s:
797 p = self.unit().xyz # point being rotated
798 r = self.others(axis=axis).unit() # axis being rotated around
800 ax, ay, az = r.xyz # quaternion-derived rotation matrix
801 bx, by, bz = r.times(d).xyz
802 sx, sy, sz = r.times(s).xyz
804 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)
805 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)
806 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)
807 else: # unrotated
808 x, y, z = self.xyz
809 return self.classof(x, y, z)
811 @deprecated_method
812 def rotateAround(self, axis, theta): # PYCHOK no cover
813 '''DEPRECATED, use method C{rotate}.'''
814 return self.rotate(axis, theta)
816 def times(self, factor):
817 '''Multiply this vector by a scalar.
819 @arg factor: Scale factor (C{scalar}).
821 @return: New, scaled vector (L{Vector3d}).
823 @raise TypeError: Non-scalar B{C{factor}}.
824 '''
825 return self._times(Scalar(factor=factor))
827 def _times(self, s):
828 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
829 '''
830 return self.classof(self.x * s, self.y * s, self.z * s)
832 def times_(self, other_x, *y_z):
833 '''Multiply this vector's components by separate X, Y and Z factors.
835 @arg other_x: X scale factor (C{scalar}) or a vector's
836 X, Y, and Z components as scale factors
837 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
838 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
839 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
840 ignored if B{C{other_x}} is not C{scalar}.
842 @return: New, scaled vector (L{Vector3d}).
844 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
845 '''
846 x, y, z = _other_x_y_z3(other_x, y_z)
847 return self.classof(self.x * x, self.y * y, self.z * z)
849# @deprecated_method
850# def to2ab(self): # PYCHOK no cover
851# '''DEPRECATED, use property C{Nvector.philam}.
852#
853# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
854# '''
855# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)
857# @deprecated_method
858# def to2ll(self): # PYCHOK no cover
859# '''DEPRECATED, use property C{Nvector.latlon}.
860#
861# @return: A L{LatLon2Tuple}C{(lat, lon)}.
862# '''
863# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)
865 @deprecated_method
866 def to3xyz(self): # PYCHOK no cover
867 '''DEPRECATED, use property L{xyz}.
868 '''
869 return self.xyz
871 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
872 '''Return a string representation of this vector.
874 @kwarg prec: Number of decimal places (C{int}).
875 @kwarg fmt: Enclosing format to use (C{str}).
876 @kwarg sep: Separator between components (C{str}).
878 @return: Vector as "(x, y, z)" (C{str}).
879 '''
880 t = sep.join(strs(self.xyz, prec=prec))
881 return (fmt % (t,)) if fmt else t
883 def unit(self, ll=None):
884 '''Normalize this vector to unit length.
886 @kwarg ll: Optional, original location (C{LatLon}).
888 @return: Normalized vector (L{Vector3d}).
889 '''
890 u = self._united
891 if ll:
892 u._fromll = ll
893 return u
895 @Property_RO
896 def _united(self): # __dict__ value overwritten below
897 '''(INTERNAL) Get normalized vector (L{Vector3d}).
898 '''
899 n = self.length
900 if n > EPS0 and fabs(n - _1_0) > EPS0:
901 u = self._xnamed(self.dividedBy(n))
902 u._update(False, length=_1_0, length2=_1_0, _united=u)
903 else:
904 u = self.copy()
905 u._update(False, _united=u)
906 if self._fromll:
907 u._fromll = self._fromll
908 return u
910 @Property
911 def x(self):
912 '''Get the X component (C{float}).
913 '''
914 return self._x
916 @x.setter # PYCHOK setter!
917 def x(self, x):
918 '''Set the X component, if different (C{float}).
919 '''
920 x = Float(x=x)
921 if self._x != x:
922 _update_all(self)
923 self._x = x
925 @Property
926 def xyz(self):
927 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
928 '''
929 return Vector3Tuple(self.x, self.y, self.z, name=self.name)
931 @xyz.setter # PYCHOK setter!
932 def xyz(self, xyz):
933 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
934 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
935 or a C{tuple} or C{list} of 3+ C{scalar} values).
936 '''
937 if islistuple(xyz, 3):
938 self._xyz(*xyz[:3])
939 else:
940 self._xyz(xyz)
942 def _xyz(self, x_xyz, *y_z):
943 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
944 '''
945 if len(self.__dict__) > 3: # any other than initial ._x, ._y and ._z attrs
946 _update_all(self)
947 try:
948 self._x, \
949 self._y, \
950 self._z = map1(_float0, x_xyz, *y_z) if y_z else x_xyz.xyz
951 except (AttributeError, TypeError, ValueError) as x:
952 raise VectorError(cause=x, **_xyzkwds(y_z, x_xyz=x_xyz))
953 return self
955 @Property_RO
956 def x2y2z2(self):
957 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
958 '''
959 return self.x**2, self.y**2, self.z**2
961 @Property
962 def y(self):
963 '''Get the Y component (C{float}).
964 '''
965 return self._y
967 @y.setter # PYCHOK setter!
968 def y(self, y):
969 '''Set the Y component, if different (C{float}).
970 '''
971 y = Float(y=y)
972 if self._y != y:
973 _update_all(self)
974 self._y = y
976 @Property
977 def z(self):
978 '''Get the Z component (C{float}).
979 '''
980 return self._z
982 @z.setter # PYCHOK setter!
983 def z(self, z):
984 '''Set the Z component, if different (C{float}).
985 '''
986 z = Float(z=z)
987 if self._z != z:
988 _update_all(self)
989 self._z = z
992def _other_x_y_z3(other_x, y_z):
993 '''(INTERNAL) Helper for C{Vector3dBase.apply} and C{Vector3dBase.times_}.
994 '''
995 try:
996 return map1(_float0, other_x, *y_z) if y_z else \
997 (other_x.x, other_x.y, other_x.z) # not .xyz!
998 except (AttributeError, TypeError, ValueError) as x:
999 raise _InvalidError(cause=x, **_xyzkwds(y_z, other_x=other_x))
1002def _xyzkwds(y_z, **xyz): # PYCHOK no cover
1003 '''(INTERANL) Helper for C{_other_x_y_z3} and C{Vector3dBase._xyz}.
1004 '''
1005 if y_z:
1006 d = dict(_zip((_y_, _z_), y_z)) # if y_z else {}, strict=True
1007 for x in xyz.values():
1008 d.update(x=x)
1009 return d
1010 return xyz
1012# **) MIT License
1013#
1014# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
1015#
1016# Permission is hereby granted, free of charge, to any person obtaining a
1017# copy of this software and associated documentation files (the "Software"),
1018# to deal in the Software without restriction, including without limitation
1019# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1020# and/or sell copies of the Software, and to permit persons to whom the
1021# Software is furnished to do so, subject to the following conditions:
1022#
1023# The above copyright notice and this permission notice shall be included
1024# in all copies or substantial portions of the Software.
1025#
1026# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1027# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1028# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1029# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1030# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1031# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1032# OTHER DEALINGS IN THE SOFTWARE.