"""Module for classes that represent common geometric objects or patterns."""
from itertools import zip_longest
import math
from typing import Union, Optional, List, Iterable, Tuple
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle as mpl_Circle
from matplotlib.patches import Rectangle as mpl_Rectangle
from .utilities import is_iterable
from .typing import NumberLike
[docs]def tan(degrees: NumberLike) -> float:
"""Calculate the tangent of the given degrees."""
return math.tan(math.radians(degrees))
[docs]def cos(degrees: NumberLike) -> float:
"""Calculate the cosine of the given degrees."""
return math.cos(math.radians(degrees))
[docs]def sin(degrees: NumberLike) -> float:
"""Calculate the sine of the given degrees."""
return math.sin(math.radians(degrees))
[docs]class Point:
"""A geometric point with x, y, and z coordinates/attributes."""
z: Union[int, float]
y: Union[int, float]
x: Union[int, float]
_attr_list: List[str] = ['x', 'y', 'z', 'idx', 'value']
_coord_list: List[str] = ['x', 'y', 'z']
def __init__(self, x: Union[NumberLike, tuple, 'Point'] = 0, y: NumberLike = 0, z: NumberLike = 0, idx: Optional[int]=None,
value: Optional[NumberLike]=None, as_int: bool=False):
"""
Parameters
----------
x : number-like, Point, iterable
x-coordinate or iterable type containing all coordinates. If iterable, values are assumed to be in order: (x,y,z).
y : number-like, optional
y-coordinate
idx : int, optional
Index of point. Useful for sequential coordinates; e.g. a point on a circle profile is sometimes easier to describe
in terms of its index rather than x,y coords.
value : number-like, optional
value at point location (e.g. pixel value of an image)
as_int : boolean
If True, coordinates are converted to integers.
"""
if isinstance(x, Point):
for attr in self._attr_list:
item = getattr(x, attr, None)
setattr(self, attr, item)
elif is_iterable(x):
for attr, item in zip_longest(self._attr_list, x, fillvalue=0):
setattr(self, attr, item)
else:
self.x = x
self.y = y
self.z = z
self.idx = idx
self.value = value
if as_int:
self.x = int(round(self.x))
self.y = int(round(self.y))
self.z = int(round(self.z))
[docs] def distance_to(self, thing: Union['Point', 'Circle']) -> float:
"""Calculate the distance to the given point.
Parameters
----------
thing : Circle, Point, 2 element iterable
The other thing to calculate distance to.
"""
if isinstance(thing, Circle):
return abs(np.sqrt((self.x - thing.center.x)**2 + (self.y - thing.center.y)**2) - thing.radius)
p = Point(thing)
return math.sqrt((self.x - p.x)**2 + (self.y - p.y)**2 + (self.z - p.z)**2)
[docs] def as_array(self, only_coords: bool=True) -> np.array:
"""Return the point as a numpy array."""
if only_coords:
return np.array([getattr(self, item) for item in self._coord_list])
else:
return np.array([getattr(self, item) for item in self._attr_list if (getattr(self, item) is not None)])
def __repr__(self):
return f"Point(x={self.x:3.2f}, y={self.y:3.2f}, z={self.z:3.2f})"
def __eq__(self, other):
# if all attrs equal, points considered equal
return all(getattr(self, attr) == getattr(other, attr) for attr in self._attr_list)
def __sub__(self, other):
p = Point()
for attr in self._attr_list:
try:
diff = getattr(self, attr) - getattr(other, attr)
except TypeError:
diff = None
setattr(p, attr, diff)
return p
def __mul__(self, other):
for attr in self._attr_list:
try:
self.__dict__[attr] *= other
except TypeError:
pass
def __truediv__(self, other):
for attr in self._attr_list:
val = getattr(self, attr)
try:
setattr(self, attr, val/other)
except TypeError:
pass
return self
[docs]class Circle:
"""A geometric circle with center Point, radius, and diameter."""
center: Point
radius: float
def __init__(self, center_point: Union[Point, Iterable]=(0, 0), radius: float = 0):
"""
Parameters
----------
center_point : Point, optional
Center point of the wobble circle.
radius : float, optional
Radius of the wobble circle.
"""
if center_point is None:
center_point = Point()
elif isinstance(center_point, Point) or is_iterable(center_point):
center_point = Point(center_point)
else:
raise TypeError("Circle center must be of type Point or iterable")
self.center = center_point
self.radius = radius
@property
def diameter(self) -> float:
"""Get the diameter of the circle."""
return self.radius*2
[docs] def plot2axes(self, axes, edgecolor: str='black', fill: bool=False) -> None:
"""Plot the Circle on the axes.
Parameters
----------
axes : matplotlib.axes.Axes
An MPL axes to plot to.
edgecolor : str
The color of the circle.
fill : bool
Whether to fill the circle with color or leave hollow.
"""
axes.add_patch(mpl_Circle((self.center.x, self.center.y), edgecolor=edgecolor, radius=self.radius, fill=fill))
[docs]class Vector:
"""A vector with x, y, and z coordinates."""
x: NumberLike
y: NumberLike
z: NumberLike
def __init__(self, x: NumberLike=0, y: NumberLike=0, z: NumberLike=0):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"Vector(x={self.x:.2f}, y={self.y:.2f}, z={self.z:.2f})"
[docs] def as_scalar(self) -> float:
"""Return the scalar equivalent of the vector."""
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
[docs] def distance_to(self, thing: Union[Circle, Point]) -> float:
"""Calculate the distance to the given point.
Parameters
----------
thing : Circle, Point, 2 element iterable
The other point to calculate distance to.
"""
if isinstance(thing, Circle):
return abs(np.sqrt((self.x - thing.center.x)**2 + (self.y - thing.center.y)**2) - thing.radius)
else:
p = Point(thing)
return math.sqrt((self.x - p.x)**2 + (self.y - p.y)**2 + (self.z - p.z)**2)
def __sub__(self, other):
new_x = self.x - other.x
new_y = self.y - other.y
new_z = self.z - other.z
return Vector(x=new_x, y=new_y, z=new_z)
def __add__(self, other):
new_x = self.x + other.x
new_y = self.y + other.y
new_z = self.z + other.z
return Vector(x=new_x, y=new_y, z=new_z)
[docs]def vector_is_close(vector1: Vector, vector2: Vector, delta: float=0.1) -> bool:
"""Determine if two vectors are with delta of each other; this is a simple coordinate comparison check."""
for attr in ('x', 'y', 'z'):
if np.isnan(getattr(vector1, attr)) and np.isnan(getattr(vector2, attr)):
continue
if not getattr(vector2, attr) + delta >= getattr(vector1, attr) >= getattr(vector2, attr) - delta:
return False
return True
[docs]class Line:
"""A line that is represented by two points or by m*x+b.
Notes
-----
Calculations of slope, etc are from here:
http://en.wikipedia.org/wiki/Linear_equation
and here:
http://www.mathsisfun.com/algebra/line-equation-2points.html
"""
point1: Point
point2: Point
def __init__(self, point1: Union[Point, Tuple[float, float]], point2: Union[Point, Tuple[float, float]]):
"""
Parameters
----------
point1 : Point
One point of the line
point2 : Point
Second point along the line.
"""
self.point1 = Point(point1)
self.point2 = Point(point2)
def __repr__(self):
return f'Line: p1:(x={self.point1.x:.1f}, y={self.point1.y:.1f}, z={self.point1.z:.1f}), ' \
f'p2:(x={self.point2.x:.1f}, y={self.point2.y:.1f}, z={self.point2.z:.1f})'
@property
def m(self) -> float:
"""Return the slope of the line.
m = (y1 - y2)/(x1 - x2)
From: http://www.purplemath.com/modules/slope.htm
"""
return (self.point1.y - self.point2.y) / (self.point1.x - self.point2.x)
@property
def b(self) -> float:
"""Return the y-intercept of the line.
b = y - m*x
"""
return self.point1.y - (self.m * self.point1.x)
[docs] def y(self, x) -> float:
"""Return y-value along line at position x."""
return self.m * x + self.b
[docs] def x(self, y) -> float:
"""Return x-value along line at position y."""
return (y - self.b)/self.m
@property
def center(self) -> Point:
"""Return the center of the line as a Point."""
mid_x = np.abs((self.point2.x - self.point1.x)/2 + self.point1.x)
mid_y = (self.point2.y - self.point1.y) / 2 + self.point1.y
return Point(mid_x, mid_y)
@property
def length(self) -> float:
"""Return length of the line, if finite."""
return self.point1.distance_to(self.point2)
[docs] def distance_to(self, point) -> float:
"""Calculate the minimum distance from the line to a point.
Equations are from here: http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html #14
Parameters
----------
point : Point, iterable
The point to calculate distance to.
"""
point = Point(point).as_array()
lp1 = self.point1.as_array()
lp2 = self.point2.as_array()
numerator = np.sqrt(np.sum(np.power(np.cross((lp2 - lp1), (lp1 - point)), 2)))
denominator = np.sqrt(np.sum(np.power(lp2 - lp1, 2)))
return numerator/denominator
[docs] def plot2axes(self, axes: plt.Axes, width: NumberLike=1, color: str='w') -> None:
"""Plot the line to an axes.
Parameters
----------
axes : matplotlib.axes.Axes
An MPL axes to plot to.
color : str
The color of the line.
"""
axes.plot((self.point1.x, self.point2.x), (self.point1.y, self.point2.y), linewidth=width, color=color)
[docs]class Rectangle:
"""A rectangle with width, height, center Point, top-left corner Point, and bottom-left corner Point."""
width: Union[int, float]
height: Union[int, float]
_as_int: bool
center: Point
def __init__(self, width: float, height: float, center: Union[Point, Tuple], as_int: bool=False):
"""
Parameters
----------
width : number
Width of the rectangle.
height : number
Height of the rectangle.
center : Point, iterable, optional
Center point of rectangle.
as_int : bool
If False (default), inputs are left as-is. If True, all inputs are converted to integers.
"""
if as_int:
self.width = int(np.round(width))
self.height = int(np.round(height))
else:
self.width = width
self.height = height
self._as_int = as_int
self.center = Point(center, as_int=as_int)
@property
def br_corner(self) -> Point:
"""The location of the bottom right corner."""
return Point(self.center.x + self.width / 2, self.center.y - self.height / 2, as_int=self._as_int)
@property
def bl_corner(self) -> Point:
"""The location of the bottom left corner."""
return Point(self.center.x - self.width / 2, self.center.y - self.height / 2, as_int=self._as_int)
@property
def tl_corner(self) -> Point:
"""The location of the top left corner."""
return Point(self.center.x - self.width / 2, self.center.y + self.height / 2, as_int=self._as_int)
@property
def tr_corner(self) -> Point:
"""The location of the top right corner."""
return Point(self.center.x + self.width / 2, self.center.y + self.height / 2, as_int=self._as_int)
[docs] def plot2axes(self, axes: plt.Axes, edgecolor: str='black', angle: float=0.0, fill: bool=False,
alpha: float=1, facecolor: str='g', label=None):
"""Plot the Rectangle to the axes.
Parameters
----------
axes : matplotlib.axes.Axes
An MPL axes to plot to.
edgecolor : str
The color of the circle.
angle : float
Angle of the rectangle.
fill : bool
Whether to fill the rectangle with color or leave hollow.
"""
axes.add_patch(mpl_Rectangle((self.bl_corner.x, self.bl_corner.y),
width=self.width,
height=self.height,
angle=angle,
edgecolor=edgecolor,
alpha=alpha,
facecolor=facecolor,
fill=fill,
label=label))