Source code for webtraversallibrary.geometry

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at

#   http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""Basic 2-dimensional geometric constructs: points, rectangles, etc. """
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterator, Sequence, Tuple, Union


[docs]@dataclass(order=True, frozen=True) class Point: """Represents a point in 2-dimensional plane (e.g. image).""" x: float y: float def __add__(self, other: Point) -> Point: return Point(self.x + other.x, self.y + other.y) def __sub__(self, other: Point) -> Point: return Point(self.x - other.x, self.y - other.y) def __mul__(self, scalar: float) -> Point: return Point(self.x * scalar, self.y * scalar) def __iter__(self) -> Iterator[float]: return (data for data in (self.x, self.y)) @staticmethod def zero() -> Point: return Point(0, 0)
[docs]@dataclass(frozen=True) class Rectangle: """Represents a rectangle in a 2-dimensional plane.""" minima: Point maxima: Point def __iter__(self) -> Iterator[float]: return (data for data in (self.x, self.y, self.width, self.height)) def __contains__(self, other: Union[Point, Rectangle]) -> bool: return self.contains(other)
[docs] def contains(self, other: Union[Point, Rectangle]) -> bool: """ Tests whether the rectangle contains ``other``. :return: ``True`` if other contained in the rectangle, ``False`` otherwise. """ if isinstance(other, Point): return self.minima.x <= other.x <= self.maxima.x and self.minima.y <= other.y <= self.maxima.y if isinstance(other, Rectangle): return ( self.maxima.x >= other.maxima.x and self.maxima.y >= other.maxima.y and self.minima.x <= other.minima.x and self.minima.y <= other.minima.y ) raise TypeError(f"Expected a Rectangle or a Point, not {type(other)}")
[docs] def clip(self, other: Rectangle) -> Rectangle: """ Return a new rectangle generated by clipping this one by the bounds of ``other``. Similar to intersection, but clipping non-intersecting rectangles will result in a degenerate rectangle located on one of the edges of ``other`` :return: New, clipped ``Rectangle`` (possibly degenerate) """ return Rectangle.from_list( min(max(self.minima.x, other.minima.x), other.maxima.x), min(max(self.minima.y, other.minima.y), other.maxima.y), min(max(self.maxima.x, other.minima.x), other.maxima.x), min(max(self.maxima.y, other.minima.y), other.maxima.y), )
@property def x(self) -> float: """The x-coordinate of the lower left vertex""" return self.minima.x @property def y(self) -> float: """The y-coordinate of the lower-left vertex""" return self.minima.y @property def bounds(self) -> Tuple[float, float, float, float]: """Returns min x, min y, max x, max y""" return self.minima.x, self.minima.y, self.maxima.x, self.maxima.y @property def center(self) -> Point: """Return the midpoint of the rectangle""" return (self.minima + self.maxima) * 0.5 @property def width(self) -> float: return self.maxima.x - self.minima.x @property def height(self) -> float: return self.maxima.y - self.minima.y @property def area(self) -> float: return self.width * self.height
[docs] @staticmethod def empty() -> Rectangle: """Returns a rectangle of zero area at origo.""" return Rectangle.from_list(0, 0, 0, 0)
[docs] def resized(self, delta: float) -> Rectangle: """ Returns a resized rectangle, shrinked (inflated for ``delta<0``) by ``2*delta`` in width and in height. """ assert 2 * delta >= -min(self.width, self.height), "Operation only defined for delta >= 2*min(width, height)" delta_point = Point(delta, delta) return Rectangle(self.minima - delta_point, self.maxima + delta_point)
def __add__(self, vector: Point) -> Rectangle: """Returns a copy of the rectangle translated by ``vector``""" return Rectangle(self.minima + vector, self.maxima + vector) def __sub__(self, vector: Point) -> Rectangle: """Returns a copy of the rectangle inversly translated by ``vector``""" return Rectangle(self.minima - vector, self.maxima - vector) def __repr__(self): return f"({self.minima}), ({self.maxima})"
[docs] @staticmethod def bounding_box(rectangles: Sequence[Rectangle]) -> Rectangle: """Computes the bounding box of ``rectangles``""" if not rectangles: raise ValueError("Expected a non-empty sequence of rectangles") max_x = max(rect.maxima.x for rect in rectangles) max_y = max(rect.maxima.y for rect in rectangles) min_x = min(rect.minima.x for rect in rectangles) min_y = min(rect.minima.y for rect in rectangles) return Rectangle.from_list(max_x, max_y, min_x, min_y)
[docs] @staticmethod def intersection(rectangles: Sequence[Rectangle]) -> Rectangle: """ Computes the rectangle which is the intersection of a sequence of ``rectangles``. In case the intersection is empty, it returns an empty rectangle. """ if not rectangles or len(rectangles) < 2: raise ValueError("Expected a sequence of at least two rectangles") if Rectangle.empty() in rectangles: return Rectangle.empty() max_x = min(rect.maxima.x for rect in rectangles) max_y = min(rect.maxima.y for rect in rectangles) min_x = max(rect.minima.x for rect in rectangles) min_y = max(rect.minima.y for rect in rectangles) if max_x < min_x or max_y < min_y: return Rectangle.empty() return Rectangle.from_list(max_x, max_y, min_x, min_y)
[docs] @staticmethod def centered_at(center: Point, radius: float) -> Rectangle: """ A new square centered at ``center`` with side length ``radius``. (The technically correct term here is `Apothem <https://en.wikipedia.org/wiki/Apothem>`_.) """ if radius <= 0: raise ValueError(f"Cannot instantiate rectangle with non-positive side {2 * radius} centered at {center}") radius_point = Point(radius, radius) return Rectangle(center - radius_point, center + radius_point)
[docs] @staticmethod def from_list(*args) -> Rectangle: """Converts tuple/list (x1, y1, x2, y2) to a Rectangle""" if len(args) == 1: args = args[0] if len(args) == 4 and all(isinstance(el, (int, float)) for el in args): first_x, first_y, second_x, second_y = args else: raise ValueError("Invalid argument(s) to create rectangle from!") minima = Point(min(first_x, second_x), min(first_y, second_y)) maxima = Point(max(first_x, second_x), max(first_y, second_y)) return Rectangle(minima, maxima)