Source code for sorbetto.geometry.line

import math
from typing import Self

from matplotlib.axes import Axes
from matplotlib.figure import Figure

from sorbetto.geometry.abstract_geometric_object_2d import AbstractGeometricObject2D
from sorbetto.geometry.line_segment import LineSegment
from sorbetto.geometry.point import Point


[docs] class Line(AbstractGeometricObject2D): """ This class is used to represent a line. :math:`a x + b y + c = 0` See https://en.wikipedia.org/wiki/Line_(geometry) """ def __init__(self, a: float, b: float, c: float, name: str | None = None): """ Constructs a new line :math:`a x + b y + c = 0`. Args: a (float): the parameter :math:`a` b (float): the parameter :math:`b` c (float): the parameter :math:`c` name (str | None, optional): the name """ assert isinstance(a, float) assert isinstance(b, float) assert isinstance(c, float) self._a = a self._b = b self._c = c assert not ( math.isclose(a, 0.0, abs_tol=1e-8) and math.isclose(b, 0.0, abs_tol=1e-8) ) AbstractGeometricObject2D.__init__(self, name) @property def a(self) -> float: """ The parameter :math:`a` of the line :math:`a x + b y + c = 0` Returns: float: :math:`a` """ return self._a @property def b(self) -> float: """ The parameter :math:`b` of the line :math:`a x + b y + c = 0` Returns: float: :math:`b` """ return self._b @property def c(self) -> float: """ The parameter :math:`c` of the line :math:`a x + b y + c = 0` Returns: float: :math:`c` """ return self._c
[docs] def getX(self, y) -> float: a = self._a b = self._b c = self._c x = -(b * y + c) / a return x
[docs] def getY(self, x) -> float: a = self._a b = self._b c = self._c y = -(a * x + c) / b return y
[docs] def getNormalized(self) -> Self: """ Computes the normalized form of the line, that is :math:`a' x + b' y + c' = 0` such that :math:`a'^2 + b'^2 = 1` and :math:`(a',b',c') \\propto (a,b,c)`. Returns: Line: the line :math:`a' x + b' y + c' = 0` """ a = self._a b = self._b c = self._c k = math.hypot(a, b) return Line(a / k, b / k, c / k, self.name)
[docs] def getIntersectionWithLine(self, other: Self) -> Self | Point | None: """ Computes the intersection of the line with another one. Args: other (Self): the other line Returns: Self | Point | None: the intersection. """ # implementation of Cremer's rule # see https://en.wikipedia.org/wiki/Cramer%27s_rule a1 = self._a b1 = self._b c1 = self._c a2 = other._a b2 = other._b c2 = other._c den = a1 * b2 - b1 * a2 if den == 0.0: # lines are parallel l1 = self.normalize() l2 = other.normalize() if l1._a * l2._a + l1._b * l2._b >= 0: d = math.fabs(l1._c - l2.c) # distance between the two lines else: d = math.fabs(l1._c + l2.c) # distance between the two lines if d < 1e-8: # cparallel and onfounded lines return self else: # parallel and not confounded lines return None else: x = (c1 * b2 - b1 * c2) / den y = (c1 * a2 - a1 * c2) / den name = "intersection between {} and {}".format(self, other) return Point(x, y, name)
[docs] def getIntersectionWithAxisAlignedBox(self, extent) -> LineSegment | Point | None: """ Computes the intersection of the line with any given axis-aligned box. Args: extent (_type_): the axis-aligned box :math:`(x_{min}, x_{max}, y_{min}, y_{max})` Returns: LineSegment | Point | None: the intersection, or None if there is no intersection. """ x_min = extent[0] x_max = extent[1] assert x_max > x_min y_min = extent[2] y_max = extent[3] assert y_max > y_min a = self._a b = self._b points = list() if b != 0.0: x = x_min y = self.getY(x) if (y_min <= y) and (y <= y_max): point = x, y points.append(point) x = x_max y = self.getY(x) if (y_min <= y) and (y <= y_max): point = x, y points.append(point) if a != 0.0: y = y_min x = self.getX(y) if (x_min <= x) and (x <= x_max): point = x, y points.append(point) y = y_max x = self.getX(y) if (x_min <= x) and (x <= x_max): point = x, y points.append(point) if len(points) == 0: return None elif len(points) == 1: p = points[0] p = Point(p.x, p.y, self.name) elif len(points) == 2: p1 = points[0] p1 = Point(p1.x, p1.y, "endpoint 1") p2 = points[-1] p2 = Point(p2.x, p2.y, "endpoint 2") return LineSegment(p1, p2, self.name) else: # Find the two furthest points # Choose the point p1 arbitrarilly p1 = points[0] # Fing the point p2 that is the furthest form p1. p2 = p1 for p0 in points: d01_sq = (p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2 d21_sq = (p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 if d01_sq > d21_sq: p2 = p0 # Fing the point p1 that is the furthest form p2. for p0 in points: d02_sq = (p0[0] - p2[0]) ** 2 + (p0[1] - p2[1]) ** 2 d12_sq = (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 if d02_sq > d12_sq: p1 = p0 p1 = points[0] p1 = Point(p1.x, p1.y, "endpoint 1") p2 = points[-1] p2 = Point(p2.x, p2.y, "endpoint 2") return LineSegment(p1, p2, self.name)
[docs] def draw(self, fig: Figure, ax: Axes, extent, **plt_kwargs): """ Draws the part of the line that is within some axis-aligned box in some given Pyplot axes. Args: fig (_type_): a Pyplot Figure object ax (_type_): a Pyplot Axes object extent (_type_): the axis-aligned box :math:`(x_{min}, x_{max}, y_{min}, y_{max})` plt_kwargs: options for Pyplot's plot command. """ intersection = self.getIntersectionWithAxisAlignedBox(extent) if intersection is not None: intersection.draw(fig, ax, extent, **plt_kwargs)
def __str__(self) -> str: a = self._a b = self._b c = self._c return "line ({}) x + ({}) y + ({}) = 0".format(a, b, c)