masque/masque/traits/repeatable.py

111 lines
3.0 KiB
Python

from typing import Self, TYPE_CHECKING
from abc import ABCMeta, abstractmethod
import numpy
from numpy.typing import NDArray
from ..error import MasqueError
from .positionable import Bounded
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
if TYPE_CHECKING:
from ..repetition import Repetition
class Repeatable(metaclass=ABCMeta):
"""
Trait class for all repeatable entities
"""
__slots__ = ()
#
# Properties
#
@property
@abstractmethod
def repetition(self) -> 'Repetition | None':
"""
Repetition object, or None (single instance only)
"""
pass
# @repetition.setter
# @abstractmethod
# def repetition(self, repetition: 'Repetition | None') -> None:
# pass
#
# Methods
#
@abstractmethod
def set_repetition(self, repetition: 'Repetition | None') -> Self:
"""
Set the repetition
Args:
repetition: new value for repetition, or None (single instance)
Returns:
self
"""
pass
class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
"""
Simple implementation of `Repeatable` and extension of `Bounded` to include repetition bounds.
"""
__slots__ = _empty_slots
_repetition: 'Repetition | None'
""" Repetition object, or None (single instance only) """
@abstractmethod
def get_bounds_single(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
pass
#
# Non-abstract properties
#
@property
def repetition(self) -> 'Repetition | None':
return self._repetition
@repetition.setter
def repetition(self, repetition: 'Repetition | None') -> None:
from ..repetition import Repetition
if repetition is not None and not isinstance(repetition, Repetition):
raise MasqueError(f'{repetition} is not a valid Repetition object!')
self._repetition = repetition
#
# Non-abstract methods
#
def set_repetition(self, repetition: 'Repetition | None') -> Self:
self.repetition = repetition
return self
def get_bounds_single_nonempty(self, *args, **kwargs) -> NDArray[numpy.float64]:
"""
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
"""
bounds = self.get_bounds_single(*args, **kwargs)
assert bounds is not None
return bounds
def get_bounds(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
bounds = self.get_bounds_single(*args, **kwargs)
if bounds is not None and self.repetition is not None:
rep_bounds = self.repetition.get_bounds()
if rep_bounds is None:
return None
bounds += rep_bounds
return bounds