team-3/src/c3nav/mapdata/utils/mpl.py

127 lines
4.1 KiB
Python
Raw Normal View History

2016-12-08 16:58:08 +01:00
from abc import ABC, abstractmethod
from dataclasses import InitVar, dataclass, field
2022-04-08 00:00:55 +02:00
from typing import Literal
2016-12-08 16:58:08 +01:00
import numpy as np
2016-12-03 19:09:39 +01:00
from matplotlib.path import Path
from shapely.geometry import GeometryCollection, LinearRing, MultiPolygon, Polygon
from shapely.geometry.base import BaseGeometry
2016-12-03 19:09:39 +01:00
2016-12-07 16:11:33 +01:00
from c3nav.mapdata.utils.geometry import assert_multipolygon
2016-12-03 19:09:39 +01:00
2016-12-08 16:58:08 +01:00
class MplPathProxy(ABC):
@abstractmethod
2022-04-08 00:00:55 +02:00
def intersects_path(self, path: Path) -> bool:
2016-12-08 16:58:08 +01:00
pass
@abstractmethod
2022-04-08 00:00:55 +02:00
def contains_point(self, point: tuple[int, int]) -> bool:
pass
@abstractmethod
def contains_points(self, points: np.ndarray[tuple[int, Literal[2]], np.uint32]) -> bool:
2016-12-08 16:58:08 +01:00
pass
@dataclass(slots=True)
2016-12-08 16:58:08 +01:00
class MplPolygonPath(MplPathProxy):
polygon: InitVar[Polygon]
exterior: Path = field(init=False)
2022-04-08 00:00:55 +02:00
interiors: tuple[Path, ...] = field(init=False)
2017-11-18 22:53:17 +01:00
def __post_init__(self, polygon):
2016-12-08 16:58:08 +01:00
self.exterior = linearring_to_mpl_path(polygon.exterior)
2022-04-08 00:00:55 +02:00
self.interiors = tuple(linearring_to_mpl_path(interior) for interior in polygon.interiors)
2016-12-08 16:58:08 +01:00
2016-12-19 19:56:04 +01:00
@property
def exteriors(self):
return (self.exterior, )
def intersects_path(self, path, filled=False):
if filled:
if not self.exterior.intersects_path(path, filled=True):
return False
2016-12-08 16:58:08 +01:00
for interior in self.interiors:
if interior.contains_path(path):
return False
return True
else:
if self.exterior.intersects_path(path, filled=False):
2016-12-08 16:58:08 +01:00
return True
for interior in self.interiors:
if interior.intersects_path(path, filled=False):
return True
return False
2016-12-08 16:58:08 +01:00
def contains_points(self, points):
2022-04-08 00:00:55 +02:00
# noinspection PyTypeChecker
result = self.exterior.contains_points(points)
for interior in self.interiors:
if not result.any():
break
ix = np.argwhere(result).flatten()
result[ix] = np.logical_not(interior.contains_points(points[ix]))
return result
2016-12-08 16:58:08 +01:00
def contains_point(self, point):
if not self.exterior.contains_point(point):
return False
for interior in self.interiors:
if interior.contains_point(point):
return False
return True
@dataclass(slots=True)
class MplMultipolygonPath(MplPathProxy):
2022-04-08 00:00:55 +02:00
polygons: tuple[MplPolygonPath, ...] = field(init=False)
polygons_: InitVar[Polygon | MultiPolygon | GeometryCollection]
def __post_init__(self, polygons_):
2022-04-08 00:00:55 +02:00
self.polygons = tuple(MplPolygonPath(polygon) for polygon in assert_multipolygon(polygons_))
@property
def exteriors(self):
return tuple(polygon.exterior for polygon in self.polygons)
def intersects_path(self, path, filled=False):
for polygon in self.polygons:
if polygon.intersects_path(path, filled=filled):
return True
return False
def contains_point(self, point):
for polygon in self.polygons:
if polygon.contains_point(point):
return True
return False
def contains_points(self, points):
result = np.full((len(points),), fill_value=False, dtype=np.bool)
for polygon in self.polygons:
ix = np.argwhere(np.logical_not(result)).flatten()
result[ix] = polygon.contains_points(points[ix])
return result
def shapely_to_mpl(geometry: BaseGeometry) -> MplPathProxy:
2016-12-03 19:09:39 +01:00
"""
convert a shapely Polygon or Multipolygon to a matplotlib Path
:param geometry: shapely Polygon or Multipolygon
2016-12-08 16:58:08 +01:00
:return: MplPathProxy
2016-12-03 19:09:39 +01:00
"""
2016-12-08 16:58:08 +01:00
if isinstance(geometry, Polygon):
return MplPolygonPath(geometry)
elif isinstance(geometry, MultiPolygon) or geometry.is_empty or isinstance(geometry, GeometryCollection):
2016-12-08 16:58:08 +01:00
return MplMultipolygonPath(geometry)
raise TypeError
2016-12-03 19:09:39 +01:00
def linearring_to_mpl_path(linearring: LinearRing) -> Path:
2022-04-03 17:34:31 +02:00
return Path(np.array(linearring.coords),
(Path.MOVETO, *([Path.LINETO] * (len(linearring.coords)-2)), Path.CLOSEPOLY), readonly=True)