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