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
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:
self.lines = 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).
"""
return min(
self._distance_point_to_line(point=(x, y), line=line) for line in self.lines
)
def _distance_point_to_line(
self,
point: Iterable[Number],
line: Iterable[Iterable[Number]],
) -> float:
"""
Calculate the minimum distance between a point and a line.
Parameters
----------
point : Iterable[numbers.Number]
Coordinates of a point in 2D space.
line : Iterable[Iterable[numbers.Number]]
Coordinates of the endpoints of a line in 2D space.
Returns
-------
float
The minimum distance between the point and the line.
Notes
-----
Implementation based on `this VBA code`_.
.. _this VBA code: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/source.vba
"""
start, end = line
line_mag = self._euclidean_distance(start, end)
if line_mag < 0.00000001:
# Arbitrarily large value
return 9999
px, py = point
x1, y1 = start
x2, y2 = end
u1 = ((px - x1) * (x2 - x1)) + ((py - y1) * (y2 - y1))
u = u1 / (line_mag * line_mag)
if (u < 0.00001) or (u > 1):
# closest point does not fall within the line segment, take the shorter
# distance to an endpoint
distance = min(
self._euclidean_distance(point, start),
self._euclidean_distance(point, end),
)
else:
# Intersecting point is on the line, use the formula
ix = x1 + u * (x2 - x1)
iy = y1 + u * (y2 - y1)
distance = self._euclidean_distance(point, (ix, iy))
return distance
[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