#!/usr/bin/env python3
#
# Copyright 2021 Graviti. Licensed under MIT License.
#
"""PointList2D, Polygon2D.
:class:`PointList2D` contains a list of 2D points.
:class:`Polygon` contains the coordinates of the vertexes of the polygon
and provides :meth:`Polygon2D.area` to calculate the area of the polygon.
"""
from typing import Dict, Iterable, List, Optional, Type, TypeVar
from ..utility import UserMutableSequence, common_loads
from .box import Box2D
from .vector import Vector2D
_T = TypeVar("_T", bound=Vector2D)
[docs]class PointList2D(UserMutableSequence[_T]):
"""This class defines the concept of PointList2D.
:class:`PointList2D` contains a list of 2D points.
Arguments:
points: A list of 2D points.
"""
_P = TypeVar("_P", bound="PointList2D[_T]")
_ElementType: Type[_T]
def __init__(
self,
points: Optional[Iterable[Iterable[float]]] = None,
) -> None:
self._data = []
if points is None:
return
for point in points:
self._data.append(self._ElementType(*point))
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return False
return self._data.__eq__(other._data)
def _loads(self: _P, contents: List[Dict[str, float]]) -> None:
self._data = []
for point in contents:
self._data.append(self._ElementType.loads(point))
[docs] @classmethod
def loads(cls: Type[_P], contents: List[Dict[str, float]]) -> _P:
"""Load a :class:`PointList2D` from a list of dictionaries.
Arguments:
contents: A list of dictionaries containing the coordinates of the vertexes
of the polygon::
[
{
"x": ...
"y": ...
},
...
]
Returns:
The loaded :class:`PointList2D` object.
"""
return common_loads(cls, contents)
[docs] def dumps(self) -> List[Dict[str, float]]:
"""Dumps a :class:`PointList2D` into a point list.
Returns:
A list of dictionaries containing the coordinates of the vertexes
of the polygon within the point list.
"""
return [point.dumps() for point in self._data]
[docs] def bounds(self) -> Box2D:
"""Calculate the bounds of point list.
Returns:
The bounds of point list.
"""
x_min = x_max = self._data[0].x
y_min = y_max = self._data[0].y
for point in self._data:
if point.x < x_min:
x_min = point.x
elif point.x > x_max:
x_max = point.x
if point.y < y_min:
y_min = point.y
elif point.y > y_max:
y_max = point.y
return Box2D(x_min, y_min, x_max, y_max)
[docs]class Polygon2D(PointList2D[Vector2D]):
"""This class defines the concept of Polygon2D.
:class:`Polygon2D` contains the coordinates of the vertexes of the polygon and provides
:meth:`Polygon2D.area` to calculate the area of the polygon.
Examples:
>>> Polygon2D([[1, 2], [2, 3], [2, 2]])
Polygon2D [
Vector2D(1, 2),
Vector2D(2, 3),
Vector2D(2, 2)
]
"""
_P = TypeVar("_P", bound="Polygon2D")
_ElementType = Vector2D
[docs] @classmethod
def loads(cls: Type[_P], contents: List[Dict[str, float]]) -> _P:
"""Load a :class:`Polygon2D` from a list of dictionaries.
Arguments:
contents: A list of dictionaries containing the coordinates
of the vertexes of the polygon.
Returns:
The loaded :class:`Polygon2D` object.
Examples:
>>> contents = [{"x": 1.0, "y": 1.0}, {"x": 2.0, "y": 2.0}, {"x": 2.0, "y": 3.0}]
>>> Polygon2D.loads(contents)
Polygon2D [
Vector2D(1.0, 1.0),
Vector2D(2.0, 2.0),
Vector2D(2.0, 3.0)
]
"""
return common_loads(cls, contents)
[docs] def area(self) -> float:
"""Return the area of the polygon.
The area is positive if the rotating direction of the points is counterclockwise,
and negative if clockwise.
Returns:
The area of the polygon.
Examples:
>>> polygon = Polygon2D([[1, 2], [2, 2], [2, 3]])
>>> polygon.area()
0.5
"""
area = 0.0
for i in range(len(self._data)):
# pylint: disable=invalid-name
x1, y1 = self._data[i - 1]
x2, y2 = self._data[i]
area += x1 * y2 - x2 * y1
return area / 2