Source code for data_morph.shapes.bases.line_collection
"""Base class for shapes that are composed of lines."""
from numbers import Number
from typing import Iterable
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from ...plotting.style import plot_with_custom_style
from .shape import Shape
[docs]
class LineCollection(Shape):
"""
Class representing a shape consisting of one or more lines.
Parameters
----------
*lines : Iterable[Iterable[numbers.Number]]
An iterable of two (x, y) pairs representing the endpoints
of a line.
"""
def __init__(self, *lines: Iterable[Iterable[Number]]) -> None:
# check that lines with the same starting and ending points raise an error
for line in lines:
start, end = line
if np.allclose(start, end):
raise ValueError(f'Line {line} has the same start and end point')
self.lines = np.array(lines)
"""Iterable[Iterable[numbers.Number]]: An iterable
of two (x, y) pairs representing the endpoints of a line."""
def __repr__(self) -> str:
return self._recursive_repr('lines')
[docs]
def distance(self, x: Number, y: Number) -> float:
"""
Calculate the minimum distance from the lines of this shape
to a point (x, y).
Parameters
----------
x, y : numbers.Number
Coordinates of a point in 2D space.
Returns
-------
float
The minimum distance from the lines of this shape to the
point (x, y).
Notes
-----
Implementation based on `this Stack Overflow answer`_.
.. _this Stack Overflow answer: https://stackoverflow.com/a/58781995
"""
point = np.array([x, y])
start_points = self.lines[:, 0, :]
end_points = self.lines[:, 1, :]
tangent_vector = end_points - start_points
normalized_tangent_vectors = np.divide(
tangent_vector,
np.hypot(tangent_vector[:, 0], tangent_vector[:, 1]).reshape(-1, 1),
)
# row-wise dot products of 2D vectors
signed_parallel_distance_start = np.multiply(
start_points - point, normalized_tangent_vectors
).sum(axis=1)
signed_parallel_distance_end = np.multiply(
point - end_points, normalized_tangent_vectors
).sum(axis=1)
clamped_parallel_distance = np.maximum.reduce(
[
signed_parallel_distance_start,
signed_parallel_distance_end,
np.zeros(signed_parallel_distance_start.shape[0]),
]
)
# row-wise cross products of 2D vectors
perpendicular_distance_component = np.cross(
point - start_points, normalized_tangent_vectors
)
return np.min(
np.hypot(clamped_parallel_distance, perpendicular_distance_component)
)
[docs]
@plot_with_custom_style
def plot(self, ax: Axes = None) -> Axes:
"""
Plot the shape.
Parameters
----------
ax : matplotlib.axes.Axes, optional
An optional :class:`~matplotlib.axes.Axes` object to plot on.
Returns
-------
matplotlib.axes.Axes
The :class:`~matplotlib.axes.Axes` object containing the plot.
"""
if not ax:
fig, ax = plt.subplots(layout='constrained')
fig.get_layout_engine().set(w_pad=0.2, h_pad=0.2)
_ = ax.axis('equal')
for start, end in self.lines:
ax.plot(*list(zip(start, end)), 'k-')
return ax