from __future__ import annotations
from typing import Generic, TypeVar, Iterable, Union, Type
import abc
import copy
import numpy as np
from fibomat.linalg.vectors import Vector, DimVector, FloatTypes, VectorValueError
from fibomat.units import LengthQuantity
VectorT = TypeVar('VectorT', Vector, DimVector)
ScalarT = TypeVar('ScalarT', int, float, np.integer, np.float, LengthQuantity)
SelfT = TypeVar('SelfT', bound='BoundingBoxBase')
[docs]class BoundingBoxBase(Generic[VectorT, ScalarT], abc.ABC):
_VectorClass: Type[VectorT]
[docs] def __init__(self, lower_left: np.ndarray, upper_right: np.ndarray):
self._lower_left = self._VectorClass(lower_left)
self._upper_right = self._VectorClass(upper_right)
self._is_valid()
[docs] @classmethod
@abc.abstractmethod
def from_points(cls, points: Iterable[VectorT]) -> BoundingBoxBase[VectorT]:
"""
Constructs rectangular bounding box containing all `points`
Args:
points (VectorArrayLike): points which should be included in bounding box
description (str, optional): description
Returns:
(BoundingBox): new `BoundingBox`
"""
raise NotImplementedError
def _is_valid(self):
if self._lower_left.x > self._upper_right.x \
or self._lower_left.y > self._upper_right.y:
raise ValueError('Invalid coordinates for bounding box')
def __eq__(self, other: object) -> bool:
if not isinstance(other, BoundingBoxBase):
raise NotImplementedError
return self._lower_left.close_to(other._lower_left) and self._upper_right.close_to(other._upper_right)
[docs] def close_to(self, other: object) -> bool:
return self == other
def __repr__(self):
return 'BoundingBox(lower_left=({}, {}), upper_right=({}, {}))'.format(
self._lower_left.x, self._lower_left.y, self._upper_right.x, self._upper_right.y
)
@property
def lower_left(self) -> VectorT:
"""
Vector: lower left coordinate
Access:
get
"""
return self._lower_left
@property
def upper_right(self) -> VectorT:
"""
Vector: upper richt coordinate
Access:
get
"""
return self._upper_right
@property
def width(self) -> ScalarT:
"""
float: with of bounding box
Access:
get
"""
return self._upper_right[0] - self._lower_left[0]
@property
def height(self) -> ScalarT:
"""
float: with of bounding box
Access:
get
"""
return self._upper_right[1] - self._lower_left[1]
@property
def center(self) -> VectorT:
"""
Vector: center bounding box
Access:
get
"""
return (self._upper_right + self._lower_left) / 2
[docs] def clone(self: SelfT) -> SelfT:
"""
Clones the bounding box
Returns:
(BoundingBox): cloned box
"""
return copy.deepcopy(self)
[docs] def scaled(self: SelfT, val: float) -> SelfT:
"""
Return a scaled version of the `BoundingBox`.
Args:
val: scale factor
Returns:
BoundingBox
"""
val = float(val)/4
shift = (self.width * val, self.height * val)
return self.__class__(self._lower_left - shift, self._upper_right + shift)
[docs] def overlaps_with(self, other: BoundingBoxBase) -> bool:
"""
Checks if this bounding box overlaps with other.
Args:
other: other bounding box
Returns:
bool: True if self and other overlap
"""
overlap_x = True
overlap_y = True
if self._lower_left.x > other._upper_right.x or self._upper_right.x < other._lower_left.x:
overlap_x = False
if self._lower_left.y > other._upper_right.y or self._upper_right.y < other._lower_left.y:
overlap_y = False
return overlap_x and overlap_y
[docs] def contains(self, other: Union[BoundingBoxBase, VectorT]) -> bool:
"""
Checks if this bounding box contains other. Alternatively, you can use `in` syntax. Note, that a box is contains
always itself. ::
box_1 = BoundingBox([1, 2], [3, 4])
box_2 = BoundingBox([1, 3], [0, 4])
print(box_1.contains(box_2)
# or
print(box_2 in box_1)
Args:
other (BoundingBox): other box
Returns:
(bool): True if `self` contains `other`
"""
if isinstance(other, self.__class__):
if self._lower_left.x <= other._lower_left.x \
and self._lower_left.y <= other._lower_left.y \
and self._upper_right.x >= other._upper_right.x \
and self._upper_right.y >= other._upper_right.y:
return True
return False
else:
try:
other = self._VectorClass(other)
return (
self._lower_left.x <= other.x <= self._upper_right.x
and self._lower_left.y <= other.y <= self._upper_right.y
)
except VectorValueError as error:
raise TypeError('other must be a instance of BoundingBox or VectorT.') from error
def __contains__(self, other: BoundingBoxBase) -> bool:
return self.contains(other)
# TODO: allow list of vectors here.
[docs] def extended(self: SelfT, other: Union[BoundingBoxBase, VectorT, Iterable[VectorT]]) -> SelfT:
"""
Return a extended bounding box so that `self` and other are contained
Args:
other (BoundingBox): other box
"""
def _extend_with_vector(bbox, vector) -> None:
if vector.x < bbox._lower_left.x:
bbox._lower_left = self._VectorClass(vector.x, bbox._lower_left.y)
if vector.x > bbox._upper_right.x:
bbox._upper_right = self._VectorClass(vector.x, bbox._upper_right.y)
if vector.y < bbox._lower_left.y:
bbox._lower_left = self._VectorClass(bbox._lower_left.x, vector.y)
if vector.y > bbox._upper_right.y:
bbox._upper_right = self._VectorClass(bbox._upper_right.x, vector.y)
clone = self.clone()
if isinstance(other, self.__class__):
if other not in clone:
if other._lower_left.x < clone._lower_left.x:
clone._lower_left = self._VectorClass(other._lower_left.x, clone._lower_left.y)
if other._lower_left.y < clone._lower_left.y:
clone._lower_left = self._VectorClass(clone._lower_left.x, other._lower_left.y)
if other._upper_right.x > clone._upper_right.x:
clone._upper_right = self._VectorClass(other._upper_right.x, clone._upper_right.y)
if other._upper_right.y > clone._upper_right.y:
clone._upper_right = self._VectorClass(clone._upper_right.x, other._upper_right.y)
else:
try:
other = self._VectorClass(other)
except VectorValueError:
# other is maybe an Iterable[VectorT]
try:
for p in other:
_extend_with_vector(clone, self._VectorClass(p))
except (TypeError, VectorValueError) as error:
raise TypeError('other must be SelfT, VectorT or Iterable[VectorT].') from error
else:
_extend_with_vector(clone, other)
return clone