Coverage for pygeodesy/vector3dBase.py: 91%
281 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-08-24 15:53 -0400
« prev ^ index » next coverage.py v7.6.0, created at 2024-08-24 15:53 -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, map2, \
12 _signOf, _zip
13from pygeodesy.constants import EPS, EPS0, INT0, PI, PI2, _copysignINF, \
14 _float0, isnear0, isnear1, isneg0, \
15 _pos_self, _1_0
16from pygeodesy.errors import CrossError, VectorError, _xcallable, _xError
17from pygeodesy.fmath import euclid_, fdot, hypot_, hypot2_
18from pygeodesy.interns import _coincident_, _colinear_, _COMMASPACE_, _xyz_
19from pygeodesy.lazily import _ALL_LAZY, _ALL_DOCS, _ALL_MODS as _MODS, \
20 _sys_version_info2
21from pygeodesy.named import _NamedBase, _NotImplemented, _xother3
22# from pygeodesy.namedTuples import Vector3Tuple # _MODS
23from pygeodesy.props import deprecated_method, Property, Property_RO, \
24 property_doc_, property_RO, _update_all
25from pygeodesy.streprs import Fmt, strs, unstr
26from pygeodesy.units import Float, Scalar
27from pygeodesy.utily import sincos2, atan2, fabs
29from math import ceil as _ceil, floor as _floor, trunc as _trunc
31__all__ = _ALL_LAZY.vector3dBase
32__version__ = '24.08.18'
35class Vector3dBase(_NamedBase): # sync __methods__ with .fsums.Fsum
36 '''(INTERNAL) Generic 3-D vector base class.
37 '''
38 _crosserrors = True # un/set by .errors.crosserrors
40 _ll = None # original latlon, '_fromll'
41# _x = INT0 # X component
42# _y = INT0 # Y component
43# _z = INT0 # Z component
45 def __init__(self, x_xyz, y=INT0, z=INT0, ll=None, **name):
46 '''New L{Vector3d} or C{Vector3dBase} instance.
48 The vector may be normalised or use x, y, z for position and
49 distance from earth centre or height relative to the surface
50 of the earth' sphere or ellipsoid.
52 @arg x_xyz: X component of vector (C{scalar}) or a (3-D) vector
53 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
54 L{Vector3Tuple}, L{Vector4Tuple} or a C{tuple} or
55 C{list} of 3+ C{scalar} items).
56 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}
57 is not C{scalar}, otherwise same units as B{C{x_xyz}}.
58 @kwarg z: Z 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 ll: Optional latlon reference (C{LatLon}).
61 @kwarg name: Optional C{B{name}=NN} (C{str}).
63 @raise VectorError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}.
64 '''
65 self._x, \
66 self._y, \
67 self._z = _xyz3(type(self), x_xyz, y, z) if isscalar(x_xyz) else \
68 _xyz3(type(self), x_xyz)
69 if ll:
70 self._ll = ll
71 if name:
72 self.name = name
74 def __abs__(self):
75 '''Return the norm of this vector.
77 @return: Norm, unit length (C{float});
78 '''
79 return self.length
81 def __add__(self, other):
82 '''Add this to an other vector (L{Vector3d}).
84 @return: Vectorial sum (L{Vector3d}).
86 @raise TypeError: Incompatible B{C{other}} C{type}.
87 '''
88 return self.plus(other)
90 def __bool__(self): # PYCHOK PyChecker
91 '''Is this vector non-zero?
93 @see: Method C{bools}.
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 return _signOf(self.length, self._other_cmp(other))
113 cmp = __cmp__
115 def __divmod__(self, other): # PYCHOK no cover
116 '''Not implemented.'''
117 return _NotImplemented(self, other)
119 def __eq__(self, other):
120 '''Is this vector equal to an other vector?
122 @arg other: The other vector (L{Vector3d}).
124 @return: C{True} if equal, C{False} otherwise.
126 @raise TypeError: Incompatible B{C{other}} C{type}.
127 '''
128 return self.isequalTo(other, eps=EPS0)
130 def __float__(self): # PYCHOK no cover
131 '''Not implemented, see method C{float}.'''
132 return _NotImplemented(self) # must return C{float}
134 def __floor__(self): # PYCHOK no cover
135 '''Return a vector with the C{floor} of these components.
137 @return: Floor-ed (L{Vector3d}).
138 '''
139 return self._mapped(_floor)
141 def __floordiv__(self, other): # PYCHOK no cover
142 '''Not implemented.'''
143 return _NotImplemented(self, other)
145 def __format__(self, *other): # PYCHOK no cover
146 '''Not implemented.'''
147 return _NotImplemented(self, *other)
149 def __ge__(self, other):
150 '''Is this vector longer than or equal to an other vector?
152 @arg other: The other vector (L{Vector3d}).
154 @return: C{True} if so, C{False} otherwise.
156 @raise TypeError: Incompatible B{C{other}} C{type}.
157 '''
158 return self.length >= self._other_cmp(other)
160# def __getitem__(self, key):
161# '''Return C{item} at index or slice C{[B{key}]}.
162# '''
163# return self.xyz[key]
165 def __gt__(self, other):
166 '''Is this vector longer than an other vector?
168 @arg other: The other vector (L{Vector3d}).
170 @return: C{True} if so, C{False} otherwise.
172 @raise TypeError: Incompatible B{C{other}} C{type}.
173 '''
174 return self.length > self._other_cmp(other)
176 def __hash__(self): # PYCHOK no cover
177 '''Return this instance' C{hash}.
178 '''
179 return hash(self.xyz) # XXX id(self)?
181 def __iadd__(self, other):
182 '''Add this and an other vector I{in-place}, C{this += B{other}}.
184 @arg other: The other vector (L{Vector3d}).
186 @raise TypeError: Incompatible B{C{other}} C{type}.
187 '''
188 return self._xyz(self.plus(other))
190 def __ifloordiv__(self, other): # PYCHOK no cover
191 '''Not implemented.'''
192 return _NotImplemented(self, other)
194 def __imatmul__(self, other): # PYCHOK Python 3.5+
195 '''Cross multiply this and an other vector I{in-place}, C{this @= B{other}}.
197 @arg other: The other vector (L{Vector3d}).
199 @raise TypeError: Incompatible B{C{other}} C{type}.
201 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 397+, 2022 p. 578+.
202 '''
203 return self._xyz(self.cross(other))
205 def __imod__(self, other): # PYCHOK no cover
206 '''Not implemented.'''
207 return _NotImplemented(self, other)
209 def __imul__(self, scalar):
210 '''Multiply this vector by a scalar I{in-place}, C{this *= B{scalar}}.
212 @arg scalar: Factor (C{scalar}).
214 @raise TypeError: Non-scalar B{C{scalar}}.
215 '''
216 return self._xyz(self.times(scalar))
218 def __int__(self): # PYCHOK no cover
219 '''Not implemented, see method C{ints}.'''
220 return _NotImplemented(self) # must return C{int}
222 def __ipow__(self, other, *mod): # PYCHOK no cover
223 '''Not implemented.'''
224 return _NotImplemented(self, other, *mod)
226 def __isub__(self, other):
227 '''Subtract an other vector from this one I{in-place}, C{this -= B{other}}.
229 @arg other: The other vector (L{Vector3d}).
231 @raise TypeError: Incompatible B{C{other}} C{type}.
232 '''
233 return self._xyz(self.minus(other))
235# def __iter__(self):
236# '''Return an C{iter}ator over this vector's components.
237# '''
238# return iter(self.xyz3)
240 def __itruediv__(self, scalar):
241 '''Divide this vector by a scalar I{in-place}, C{this /= B{scalar}}.
243 @arg scalar: The divisor (C{scalar}).
245 @raise TypeError: Non-scalar B{C{scalar}}.
246 '''
247 return self._xyz(self.dividedBy(scalar))
249 def __le__(self, other): # Python 3+
250 '''Is this vector shorter than or equal to an other vector?
252 @arg other: The other vector (L{Vector3d}).
254 @return: C{True} if so, C{False} otherwise.
256 @raise TypeError: Incompatible B{C{other}} C{type}.
257 '''
258 return self.length <= self._other_cmp(other)
260# def __len__(self):
261# '''Return C{3}, always.
262# '''
263# return len(self.xyz)
265 def __lt__(self, other): # Python 3+
266 '''Is this vector shorter than an other vector?
268 @arg other: The other vector (L{Vector3d}).
270 @return: C{True} if so, C{False} otherwise.
272 @raise TypeError: Incompatible B{C{other}} C{type}.
273 '''
274 return self.length < self._other_cmp(other)
276 def __matmul__(self, other): # PYCHOK Python 3.5+
277 '''Compute the cross product of this and an other vector, C{this @ B{other}}.
279 @arg other: The other vector (L{Vector3d}).
281 @return: Cross product (L{Vector3d}).
283 @raise TypeError: Incompatible B{C{other}} C{type}.
284 '''
285 return self.cross(other)
287 def __mod__(self, other): # PYCHOK no cover
288 '''Not implemented.'''
289 return _NotImplemented(self, other)
291 def __mul__(self, scalar):
292 '''Multiply this vector by a scalar, C{this * B{scalar}}.
294 @arg scalar: Factor (C{scalar}).
296 @return: Product (L{Vector3d}).
297 '''
298 return self.times(scalar)
300 def __ne__(self, other):
301 '''Is this vector not equal to an other vector?
303 @arg other: The other vector (L{Vector3d}).
305 @return: C{True} if so, C{False} otherwise.
307 @raise TypeError: Incompatible B{C{other}} C{type}.
308 '''
309 return not self.isequalTo(other, eps=EPS0)
311 def __neg__(self):
312 '''Return the opposite of this vector.
314 @return: A negated copy (L{Vector3d})
315 '''
316 return self.negate()
318 def __pos__(self): # PYCHOK no cover
319 '''Return this vector I{as-is} or a copy.
321 @return: This instance (L{Vector3d})
322 '''
323 return self if _pos_self else self.copy()
325 def __pow__(self, other, *mod): # PYCHOK no cover
326 '''Not implemented.'''
327 return _NotImplemented(self, other, *mod)
329 __radd__ = __add__ # PYCHOK no cover
331 def __rdivmod__ (self, other): # PYCHOK no cover
332 '''Not implemented.'''
333 return _NotImplemented(self, other)
335# def __repr__(self):
336# '''Return the default C{repr(this)}.
337# '''
338# return self.toRepr()
340 def __rfloordiv__(self, other): # PYCHOK no cover
341 '''Not implemented.'''
342 return _NotImplemented(self, other)
344 def __rmatmul__(self, other): # PYCHOK Python 3.5+
345 '''Compute the cross product of an other and this vector, C{B{other} @ this}.
347 @arg other: The other vector (L{Vector3d}).
349 @return: Cross product (L{Vector3d}).
351 @raise TypeError: Incompatible B{C{other}} C{type}.
352 '''
353 return self.others(other).cross(self)
355 def __rmod__(self, other): # PYCHOK no cover
356 '''Not implemented.'''
357 return _NotImplemented(self, other)
359 __rmul__ = __mul__
361 def __round__(self, *ndigits): # PYCHOK no cover
362 '''Return a vector with these components C{rounded}.
364 @arg ndigits: Optional number of digits (C{int}).
366 @return: Rounded (L{Vector3d}).
367 '''
368 # <https://docs.Python.org/3.12/reference/datamodel.html?#object.__round__>
369 return self.classof(*(round(_, *ndigits) for _ in self.xyz3))
371 def __rpow__(self, other, *mod): # PYCHOK no cover
372 '''Not implemented.'''
373 return _NotImplemented(self, other, *mod)
375 def __rsub__(self, other): # PYCHOK no cover
376 '''Subtract this vector from an other vector, C{B{other} - this}.
378 @arg other: The other vector (L{Vector3d}).
380 @return: Difference (L{Vector3d}).
382 @raise TypeError: Incompatible B{C{other}} C{type}.
383 '''
384 return self.others(other).minus(self)
386 def __rtruediv__(self, scalar): # PYCHOK no cover
387 '''Not implemented.'''
388 return _NotImplemented(self, scalar)
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 def __trunc__(self): # PYCHOK no cover
418 '''Return a vector with the C{trunc} of these components.
420 @return: Trunc-ed (L{Vector3d}).
421 '''
422 return self._mapped(_trunc)
424 if _sys_version_info2 < (3, 0): # PYCHOK no cover
425 # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
426 __div__ = __truediv__
427 __idiv__ = __itruediv__
428 __long__ = __int__
429 __nonzero__ = __bool__
430 __rdiv__ = __rtruediv__
432 def angleTo(self, other, vSign=None, wrap=False):
433 '''Compute the angle between this and an other vector.
435 @arg other: The other vector (L{Vector3d}).
436 @kwarg vSign: Optional vector, if supplied (and out of the
437 plane of this and the other), angle is signed
438 positive if this->other is clockwise looking
439 along vSign or negative in opposite direction,
440 otherwise angle is unsigned.
441 @kwarg wrap: If C{True}, wrap/unroll the angle to +/-PI (C{bool}).
443 @return: Angle (C{radians}).
445 @raise TypeError: If B{C{other}} or B{C{vSign}} not a L{Vector3d}.
446 '''
447 x = self.cross(other)
448 s = x.length
449 # use vSign as reference to set sign of s
450 if s and vSign and x.dot(vSign) < 0:
451 s = -s
453 a = atan2(s, self.dot(other))
454 if wrap and fabs(a) > PI:
455 a -= _copysign(PI2, a)
456 return a
458 def apply(self, fun2, other_x, *y_z, **fun2_kwds):
459 '''Apply a 2-argument function pairwise to the components
460 of this and an other vector.
462 @arg fun2: 2-Argument callable (C{any(scalar, scalar}),
463 return a C{scalar} or L{INT0} result.
464 @arg other_x: Other X component (C{scalar}) or a vector
465 with X, Y and Z components (C{Cartesian},
466 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
467 L{Vector3Tuple} or L{Vector4Tuple}).
468 @arg y_z: Other Y and Z components, positional (C{scalar}, C{scalar}).
469 @kwarg fun2_kwds: Optional keyword arguments for B{C{fun2}}.
471 @return: New, applied vector (L{Vector3d}).
473 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
474 '''
475 _xcallable(fun2=fun2)
476 if fun2_kwds:
477 def _f2(a, b):
478 return fun2(a, b, **fun2_kwds)
479 else:
480 _f2 = fun2
482 xyz = _xyz3(self.apply, other_x, *y_z)
483 xyz = (_f2(a, b) for a, b in _zip(self.xyz3, xyz)) # strict=True
484 return self.classof(*xyz)
486 def bools(self):
487 '''Return the vector with C{bool} components.
488 '''
489 return self._mapped(bool)
491 def cross(self, other, raiser=None, eps0=EPS): # raiser=NN
492 '''Compute the cross product of this and an other vector.
494 @arg other: The other vector (L{Vector3d}).
495 @kwarg raiser: Optional, L{CrossError} label if raised (C{str}, non-L{NN}).
496 @kwarg eps0: Near-zero tolerance (C{scalar}), same units as C{x}, C{y} and
497 C{z}.
499 @return: Cross product (L{Vector3d}).
501 @raise CrossError: Zero or near-zero cross product and if B{C{raiser}} and
502 L{crosserrors<pygeodesy.crosserrors>} are both C{True}.
504 @raise TypeError: Incompatible B{C{other}} C{type}.
505 '''
506 X, Y, Z = self.others(other).xyz3
507 x, y, z = self.xyz3
508 xyz = ((y * Z - z * Y),
509 (z * X - x * Z),
510 (x * Y - y * X))
512 if raiser and self.crosserrors and eps0 > 0 \
513 and max(map(fabs, xyz)) < eps0:
514 r = other._fromll or other
515 s = self._fromll or self
516 t = self.isequalTo(other, eps=eps0)
517 t = _coincident_ if t else _colinear_
518 raise CrossError(raiser, s, other=r, txt=t)
520 return self.classof(*xyz) # name__=self.cross
522 @property_doc_('''raise or ignore L{CrossError} exceptions (C{bool}).''')
523 def crosserrors(self):
524 '''Get L{CrossError} exceptions (C{bool}).
525 '''
526 return self._crosserrors
528 @crosserrors.setter # PYCHOK setter!
529 def crosserrors(self, raiser):
530 '''Raise or ignore L{CrossError} exceptions (C{bool}).
531 '''
532 self._crosserrors = bool(raiser)
534 def dividedBy(self, divisor):
535 '''Divide this vector by a scalar.
537 @arg divisor: The divisor (C{scalar}).
539 @return: New, scaled vector (L{Vector3d}).
541 @raise TypeError: Non-scalar B{C{divisor}}.
543 @raise VectorError: Invalid or zero B{C{divisor}}.
544 '''
545 d = Scalar(divisor=divisor)
546 try:
547 return self._times(_1_0 / d)
548 except (ValueError, ZeroDivisionError) as x:
549 raise VectorError(divisor=divisor, cause=x)
551 def dot(self, other):
552 '''Compute the dot (scalar) product of this and an other vector.
554 @arg other: The other vector (L{Vector3d}).
556 @return: Dot product (C{float}).
558 @raise TypeError: Incompatible B{C{other}} C{type}.
559 '''
560 return self.length2 if other is self else fdot(
561 self.xyz3, *self.others(other).xyz3)
563 @deprecated_method
564 def equals(self, other, units=False): # PYCHOK no cover
565 '''DEPRECATED, use method C{isequalTo}.
566 '''
567 return self.isequalTo(other, units=units)
569 @Property_RO
570 def euclid(self):
571 '''I{Approximate} the length (norm, magnitude) of this vector (C{Float}).
573 @see: Properties C{length} and C{length2} and function
574 L{pygeodesy.euclid_}.
575 '''
576 return Float(euclid=euclid_(*self.xyz3))
578 def equirectangular(self, other):
579 '''I{Approximate} the difference between this and an other vector.
581 @arg other: Vector to subtract (C{Vector3dBase}).
583 @return: The length I{squared} of the difference (C{Float}).
585 @raise TypeError: Incompatible B{C{other}} C{type}.
587 @see: Property C{length2}.
588 '''
589 d = self.minus(other)
590 return Float(equirectangular=hypot2_(*d.xyz3))
592 def fabs(self):
593 '''Return the vector with C{fabs} components.
594 '''
595 return self._mapped(fabs)
597 def floats(self):
598 '''Return the vector with C{float} components.
599 '''
600 return self._mapped(_float0)
602 @Property
603 def _fromll(self):
604 '''(INTERNAL) Get the latlon reference (C{LatLon}) or C{None}.
605 '''
606 return self._ll
608 @_fromll.setter # PYCHOK setter!
609 def _fromll(self, ll):
610 '''(INTERNAL) Set the latlon reference (C{LatLon}) or C{None}.
611 '''
612 self._ll = ll or None
614 @property_RO
615 def homogeneous(self):
616 '''Get this vector's homogeneous representation (L{Vector3d}).
617 '''
618 x, y, z = self.xyz3
619 if z:
620 x = x / z # /= chokes PyChecker
621 y = y / z
622# z = _1_0
623 else:
624 if isneg0(z):
625 x = -x
626 y = -y
627 x = _copysignINF(x)
628 y = _copysignINF(y)
629# z = NAN
630 return self.classof(x, y, _1_0)
632 def intermediateTo(self, other, fraction, **unused): # height=None, wrap=False
633 '''Locate the vector at a given fraction between (or along) this
634 and an other vector.
636 @arg other: The other vector (L{Vector3d}).
637 @arg fraction: Fraction between both vectors (C{scalar},
638 0.0 for this and 1.0 for the other vector).
640 @return: Intermediate vector (L{Vector3d}).
642 @raise TypeError: Incompatible B{C{other}} C{type}.
643 '''
644 f = Scalar(fraction=fraction)
645 if isnear0(f): # PYCHOK no cover
646 r = self
647 else:
648 r = self.others(other)
649 if not isnear1(f): # self * (1 - f) + r * f
650 r = self.plus(r.minus(self)._times(f))
651 return r
653 def ints(self):
654 '''Return the vector with C{int} components.
655 '''
656 return self._mapped(int)
658 def isconjugateTo(self, other, minum=1, eps=EPS):
659 '''Determine whether this and an other vector are conjugates.
661 @arg other: The other vector (C{Cartesian}, L{Ecef9Tuple},
662 L{Vector3d}, C{Vector3Tuple} or C{Vector4Tuple}).
663 @kwarg minum: Minimal number of conjugates required (C{int}, 0..3).
664 @kwarg eps: Tolerance for equality and conjugation (C{scalar}),
665 same units as C{x}, C{y}, and C{z}.
667 @return: C{True} if both vector's components either match
668 or at least C{B{minum}} have opposite signs.
670 @raise TypeError: Incompatible B{C{other}} C{type}.
672 @see: Method C{isequalTo}.
673 '''
674 self.others(other)
675 n = 0
676 for a, b in zip(self.xyz3, other.xyz3):
677 if fabs(a + b) < eps and ((a < 0 and b > 0) or
678 (a > 0 and b < 0)):
679 n += 1 # conjugate
680 elif fabs(a - b) > eps:
681 return False # unequal
682 return bool(n >= minum)
684 def isequalTo(self, other, units=False, eps=EPS):
685 '''Check if this and an other vector are equal or equivalent.
687 @arg other: The other vector (L{Vector3d}).
688 @kwarg units: Optionally, compare the normalized, unit
689 version of both vectors.
690 @kwarg eps: Tolerance for equality (C{scalar}), same units as
691 C{x}, C{y}, and C{z}.
693 @return: C{True} if vectors are identical, C{False} otherwise.
695 @raise TypeError: Incompatible B{C{other}} C{type}.
697 @see: Method C{isconjugateTo}.
698 '''
699 if units:
700 self.others(other)
701 d = self.unit().minus(other.unit())
702 else:
703 d = self.minus(other)
704 return max(map(fabs, d.xyz3)) < eps
706 @Property_RO
707 def length(self): # __dict__ value overwritten by Property_RO C{_united}
708 '''Get the length (norm, magnitude) of this vector (C{Float}).
710 @see: Properties L{length2} and L{euclid}.
711 '''
712 return Float(length=hypot_(self.x, self.y, self.z))
714 @Property_RO
715 def length2(self): # __dict__ value overwritten by Property_RO C{_united}
716 '''Get the length I{squared} of this vector (C{Float}).
718 @see: Property L{length} and method C{equirectangular}.
719 '''
720 return Float(length2=hypot2_(self.x, self.y, self.z))
722 def _mapped(self, func):
723 '''(INTERNAL) Apply C{func} to all components.
724 '''
725 return self.classof(*map2(func, self.xyz3))
727 def minus(self, other):
728 '''Subtract an other vector from this vector.
730 @arg other: The other vector (L{Vector3d}).
732 @return: New vector difference (L{Vector3d}).
734 @raise TypeError: Incompatible B{C{other}} C{type}.
735 '''
736 return self._minus(*self.others(other).xyz3)
738 def _minus(self, x, y, z):
739 '''(INTERNAL) Helper for methods C{.minus} and C{.minus_}.
740 '''
741 return self.classof(self.x - x, self.y - y, self.z - z)
743 def minus_(self, other_x, *y_z):
744 '''Subtract separate X, Y and Z components from this vector.
746 @arg other_x: X component (C{scalar}) or a vector's
747 X, Y, and Z components (C{Cartesian},
748 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
749 L{Vector3Tuple}, L{Vector4Tuple}).
750 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
751 ignored if B{C{other_x}} is not C{scalar}.
753 @return: New, vectiorial vector (L{Vector3d}).
755 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
756 '''
757 return self._minus(*_xyz3(self.minus_, other_x, *y_z))
759 def negate(self):
760 '''Return the opposite of this vector.
762 @return: A negated copy (L{Vector3d})
763 '''
764 return self.classof(-self.x, -self.y, -self.z)
766 @Property_RO
767 def _N_vector(self):
768 '''(INTERNAL) Get the (C{nvectorBase._N_vector_})
769 '''
770 return _MODS.nvectorBase._N_vector_(*self.xyz3, name=self.name)
772 def _other_cmp(self, other):
773 '''(INTERNAL) Return the value for comparison.
774 '''
775 return other if isscalar(other) else self.others(other).length
777 def others(self, *other, **name_other_up):
778 '''Refined class comparison.
780 @arg other: The other vector (L{Vector3d}).
781 @kwarg name_other_up: Overriding C{name=other} and C{up=1}
782 keyword arguments.
784 @return: The B{C{other}} if compatible.
786 @raise TypeError: Incompatible B{C{other}} C{type}.
787 '''
788 other, name, up = _xother3(self, other, **name_other_up)
789 if not isinstance(other, Vector3dBase):
790 _NamedBase.others(self, other, name=name, up=up + 1)
791 return other
793 def plus(self, other):
794 '''Add this vector and an other vector.
796 @arg other: The other vector (L{Vector3d}).
798 @return: Vectorial sum (L{Vector3d}).
800 @raise TypeError: Incompatible B{C{other}} C{type}.
801 '''
802 return self._plus(*self.others(other).xyz3)
804 sum = plus # alternate name
806 def _plus(self, x, y, z):
807 '''(INTERNAL) Helper for methods C{.plus} and C{.plus_}.
808 '''
809 return self.classof(self.x + x, self.y + y, self.z + z)
811 def plus_(self, other_x, *y_z):
812 '''Sum of this vector and separate X, Y and Z components.
814 @arg other_x: X component (C{scalar}) or a vector's
815 X, Y, and Z components (C{Cartesian},
816 L{Ecef9Tuple}, C{Nvector}, L{Vector3d},
817 L{Vector3Tuple}, L{Vector4Tuple}).
818 @arg y_z: Y and Z components (C{scalar}, C{scalar}),
819 ignored if B{C{other_x}} is not C{scalar}.
821 @return: New, vectiorial vector (L{Vector3d}).
823 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
824 '''
825 return self._plus(*_xyz3(self.plus_, other_x, *y_z))
827 def rotate(self, axis, theta):
828 '''Rotate this vector around an axis by a specified angle.
830 @arg axis: The axis being rotated around (L{Vector3d}).
831 @arg theta: The angle of rotation (C{radians}).
833 @return: New, rotated vector (L{Vector3d}).
835 @see: U{Rotation matrix from axis and angle<https://WikiPedia.org/wiki/
836 Rotation_matrix#Rotation_matrix_from_axis_and_angle>} and
837 U{Quaternion-derived rotation matrix<https://WikiPedia.org/wiki/
838 Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix>}.
839 '''
840 s, c = sincos2(theta) # rotation angle
841 d = _1_0 - c
842 if d or s:
843 p = self.unit().xyz # point being rotated
844 r = self.others(axis=axis).unit() # axis being rotated around
846 ax, ay, az = r.xyz3 # quaternion-derived rotation matrix
847 bx, by, bz = r.times(d).xyz3
848 sx, sy, sz = r.times(s).xyz3
850 x = fdot(p, ax * bx + c, ax * by - sz, ax * bz + sy)
851 y = fdot(p, ay * bx + sz, ay * by + c, ay * bz - sx)
852 z = fdot(p, az * bx - sy, az * by + sx, az * bz + c)
853 else: # unrotated
854 x, y, z = self.xyz3
855 return self.classof(x, y, z)
857 @deprecated_method
858 def rotateAround(self, axis, theta): # PYCHOK no cover
859 '''DEPRECATED, use method C{rotate}.'''
860 return self.rotate(axis, theta)
862 def times(self, factor):
863 '''Multiply this vector by a scalar.
865 @arg factor: Scale factor (C{scalar}).
867 @return: New, scaled vector (L{Vector3d}).
869 @raise TypeError: Non-scalar B{C{factor}}.
870 '''
871 return self._times(Scalar(factor=factor))
873 def _times(self, s):
874 '''(INTERNAL) Helper for C{.dividedBy} and C{.times}.
875 '''
876 return self.classof(self.x * s, self.y * s, self.z * s)
878 def times_(self, other_x, *y_z):
879 '''Multiply this vector's components by separate X, Y and Z factors.
881 @arg other_x: X scale factor (C{scalar}) or a vector's
882 X, Y, and Z components as scale factors
883 (C{Cartesian}, L{Ecef9Tuple}, C{Nvector},
884 L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}).
885 @arg y_z: Y and Z scale factors (C{scalar}, C{scalar}),
886 ignored if B{C{other_x}} is not C{scalar}.
888 @return: New, scaled vector (L{Vector3d}).
890 @raise ValueError: Invalid B{C{other_x}} or B{C{y_z}}.
891 '''
892 x, y, z = _xyz3(self.times_, other_x, *y_z)
893 return self.classof(self.x * x, self.y * y, self.z * z)
895# @deprecated_method
896# def to2ab(self): # PYCHOK no cover
897# '''DEPRECATED, use property C{Nvector.philam}.
898#
899# @return: A L{PhiLam2Tuple}C{(phi, lam)}.
900# '''
901# return _MODS.formy.n_xyz2philam(self.x, self.y, self.z)
903# @deprecated_method
904# def to2ll(self): # PYCHOK no cover
905# '''DEPRECATED, use property C{Nvector.latlon}.
906#
907# @return: A L{LatLon2Tuple}C{(lat, lon)}.
908# '''
909# return _MODS.formy.n_xyz2latlon(self.x, self.y, self.z)
911 @deprecated_method
912 def to3xyz(self): # PYCHOK no cover
913 '''DEPRECATED, use property L{xyz}.
914 '''
915 return self.xyz
917 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected
918 '''Return a string representation of this vector.
920 @kwarg prec: Number of decimal places (C{int}).
921 @kwarg fmt: Enclosing format to use (C{str}).
922 @kwarg sep: Separator between components (C{str}).
924 @return: Vector as "(x, y, z)" (C{str}).
925 '''
926 t = sep.join(strs(self.xyz3, prec=prec))
927 return (fmt % (t,)) if fmt else t
929 def unit(self, ll=None):
930 '''Normalize this vector to unit length.
932 @kwarg ll: Optional, original location (C{LatLon}).
934 @return: Normalized vector (L{Vector3d}).
935 '''
936 u = self._united
937 if ll:
938 u._fromll = ll
939 return u
941 @Property_RO
942 def _united(self): # __dict__ value overwritten below
943 '''(INTERNAL) Get normalized vector (L{Vector3d}).
944 '''
945 n = self.length
946 if n > EPS0 and fabs(n - _1_0) > EPS0:
947 u = self._xnamed(self.dividedBy(n))
948 u._update(False, length=_1_0, length2=_1_0, _united=u)
949 else:
950 u = self.copy()
951 u._update(False, _united=u)
952 if self._fromll:
953 u._fromll = self._fromll
954 return u
956 @Property
957 def x(self):
958 '''Get the X component (C{float}).
959 '''
960 return self._x
962 @x.setter # PYCHOK setter!
963 def x(self, x):
964 '''Set the X component, if different (C{float}).
965 '''
966 x = Float(x=x)
967 if self._x != x:
968 _update_all(self, needed=3)
969 self._x = x
971 @Property
972 def xyz(self):
973 '''Get the X, Y and Z components (L{Vector3Tuple}C{(x, y, z)}).
974 '''
975 return _MODS.namedTuples.Vector3Tuple(*self.xyz3, name=self.name)
977 @xyz.setter # PYCHOK setter!
978 def xyz(self, xyz):
979 '''Set the X, Y and Z components (C{Cartesian}, L{Ecef9Tuple},
980 C{Nvector}, L{Vector3d}, L{Vector3Tuple}, L{Vector4Tuple}
981 or a C{tuple} or C{list} of 3+ C{scalar} items).
982 '''
983 self._xyz(xyz)
985 def _xyz(self, x_xyz, *y_z):
986 '''(INTERNAL) Set the C{_x}, C{_y} and C{_z} attributes.
987 '''
988 _update_all(self, needed=3)
989 self._x, self._y, self._z = _xyz3(_xyz_, x_xyz, *y_z)
990 return self
992 @property_RO
993 def xyz3(self):
994 '''Get the X, Y and Z components as C{3-tuple}.
995 '''
996 return self.x, self.y, self.z
998 @property_RO
999 def x2y2z2(self):
1000 '''Get the X, Y and Z components I{squared} (3-tuple C{(x**2, y**2, z**2)}).
1001 '''
1002 return self.x**2, self.y**2, self.z**2
1004 @Property
1005 def y(self):
1006 '''Get the Y component (C{float}).
1007 '''
1008 return self._y
1010 @y.setter # PYCHOK setter!
1011 def y(self, y):
1012 '''Set the Y component, if different (C{float}).
1013 '''
1014 y = Float(y=y)
1015 if self._y != y:
1016 _update_all(self, needed=3)
1017 self._y = y
1019 @Property
1020 def z(self):
1021 '''Get the Z component (C{float}).
1022 '''
1023 return self._z
1025 @z.setter # PYCHOK setter!
1026 def z(self, z):
1027 '''Set the Z component, if different (C{float}).
1028 '''
1029 z = Float(z=z)
1030 if self._z != z:
1031 _update_all(self, needed=3)
1032 self._z = z
1035def _xyz3(where, x_xyz, *y_z): # in .cartesianBase._rtp3
1036 '''(INTERNAL) Get , Y and Z as 3-tuple C{(x, y, z)}.
1037 '''
1038 try:
1039 xyz3 = map1(_float0, x_xyz, *y_z) if y_z else ( # islistuple for Vector*Tuple
1040 map2(_float0, x_xyz[:3]) if islistuple(x_xyz, minum=3) else
1041 x_xyz.xyz) # .xyz3
1042 except (AttributeError, TypeError, ValueError) as x:
1043 raise _xError(x, unstr(where, x_xyz, *y_z))
1044 return xyz3
1047__all__ += _ALL_DOCS(Vector3dBase)
1049# **) MIT License
1050#
1051# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1052#
1053# Permission is hereby granted, free of charge, to any person obtaining a
1054# copy of this software and associated documentation files (the "Software"),
1055# to deal in the Software without restriction, including without limitation
1056# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1057# and/or sell copies of the Software, and to permit persons to whom the
1058# Software is furnished to do so, subject to the following conditions:
1059#
1060# The above copyright notice and this permission notice shall be included
1061# in all copies or substantial portions of the Software.
1062#
1063# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1064# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1065# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1066# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1067# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1068# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1069# OTHER DEALINGS IN THE SOFTWARE.