Skip to content

Commit

Permalink
Adding tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasteuwen committed Aug 16, 2024
1 parent e991679 commit d74a2df
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 249 deletions.
78 changes: 62 additions & 16 deletions dlup/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ class _BaseGeometry:
def __init__(self, *args: Any, **kwargs: Any):
pass

@abc.abstractmethod
def from_shapely(cls, shapely_geometry):
return NotImplemented
@classmethod
def from_shapely(cls, shapely_geometry: ShapelyPoint | ShapelyPolygon) -> "_BaseGeometry":
raise NotImplementedError

def set_field(self, value: Any) -> None:
raise NotImplementedError

def get_field(self) -> None:
raise NotImplementedError

@property
def label(self) -> str:
return self.get_field("label")

@label.setter
def label(self, value: str) -> None:
if not isinstance(value, str):
raise ValueError(f"Label must be a string, got {type(value)}")
self.set_field("label", value)

@property
Expand All @@ -35,14 +43,18 @@ def index(self) -> int:

@index.setter
def index(self, value: int) -> None:
if not isinstance(value, int):
raise ValueError(f"Index must be an integer, got {type(value)}")
self.set_field("index", value)

@property
def color(self):
return self.get_field("color")

@color.setter
def color(self, value: str) -> None:
def color(self, value: tuple[int, int, int]) -> None:
if not isinstance(value, tuple) or len(value) != 3:
raise ValueError(f"Color must be an RGB tuple, got {type(value)}")
self.set_field("color", value)

def __eq__(self, other: "_BaseGeometry") -> bool:
Expand Down Expand Up @@ -75,12 +87,9 @@ def __isub__(self, other: Any) -> None:
def __repr__(self):
repr_string = f"<{self.__class__.__name__}("
parts = []
if self.label:
parts.append(f"label='{self.label}'")
if self.color:
parts.append(f"color='{self.color}'")
if self.index is not None:
parts.append(f"index={self.index}")
for field in self.fields:
value = self.get_field(field)
parts.append(f"{field}={value}")

repr_string += ", ".join(parts)

Expand All @@ -93,6 +102,7 @@ def __repr__(self):

class DlupPolygon(_dg.Polygon, _BaseGeometry):
def __init__(self, *args, **kwargs):
_BaseGeometry.__init__(self)
if SHAPELY_AVAILABLE:
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], ShapelyPolygon):
return self.from_shapely(args[0])
Expand All @@ -117,6 +127,7 @@ def __init__(self, *args, **kwargs):
for key, value in fields.items():
self.set_field(key, value)


@classmethod
def from_shapely(cls, shapely_polygon):
if not SHAPELY_AVAILABLE:
Expand Down Expand Up @@ -144,12 +155,9 @@ def __setstate__(self, state):
self.set_field(key, value)

def __copy__(self):
# Create a new instance of DlupPolygon with the same geometry
new_copy = DlupPolygon(self.get_exterior(), self.get_interiors())

for field in self.fields:
new_copy.set_field(field, self.get_field(field))

import warnings
warnings.warn("Copying a Polygon currently creates a complete new object, without reference to the previous one, and is essentially the same as a deepcopy.")
new_copy = DlupPolygon(self)
return new_copy

def __deepcopy__(self, memo):
Expand Down Expand Up @@ -190,6 +198,7 @@ def dlup_polygon_factory(polygon):

class DlupPoint(_dg.Point, _BaseGeometry):
def __init__(self, *args, **kwargs):
_BaseGeometry.__init__(self)
if SHAPELY_AVAILABLE:
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], ShapelyPoint):
return self.from_shapely(args[0])
Expand All @@ -210,6 +219,7 @@ def __init__(self, *args, **kwargs):
for key, value in fields.items():
self.set_field(key, value)


@classmethod
def from_shapely(cls, shapely_point: "ShapelyPoint"):
if not SHAPELY_AVAILABLE:
Expand All @@ -221,6 +231,42 @@ def from_shapely(cls, shapely_point: "ShapelyPoint"):
raise ValueError(f"Expected a shapely.geometry.Point, but got {type(shapely_point)}")

return cls(shapely_point.x, shapely_point.y)

def to_shapely(self):
if not SHAPELY_AVAILABLE:
raise ImportError(
"Shapely is not available, and this functionality requires it. Install it using `pip install shapely`, or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html for more information."
)

return ShapelyPoint(self.get_coordinates())

@property
def x(self):
return self.get_coordinates()[0]

@property
def y(self):
return self.get_coordinates()[1]

def __copy__(self):
# Create a new instance of DlupPolygon with the same geometry
new_copy = DlupPoint(self.x, self.y)

for field in self.fields:
new_copy.set_field(field, self.get_field(field))

return new_copy

def __deepcopy__(self, memo):
# Create a deepcopy of the geometry
new_copy = DlupPoint(copy.deepcopy(self.x), copy.deepcopy(self.y))

# Deepcopy the fields
for field in self.fields:
new_copy.set_field(field, copy.deepcopy(self.get_field(field), memo))

return new_copy


def __getstate__(self):
state = {
Expand Down
5 changes: 5 additions & 0 deletions src/exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class GeometryNotFoundError : public GeometryError {
explicit GeometryNotFoundError(const std::string &message) : GeometryError(message) {}
};

class GeometryCoordinatesError : public GeometryError {
public:
explicit GeometryCoordinatesError(const std::string &message) : GeometryError(message) {}
};

class GeometryIntersectionError : public GeometryError {
public:
explicit GeometryIntersectionError(const std::string &message) : GeometryError(message) {}
Expand Down
7 changes: 7 additions & 0 deletions src/geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,12 @@ PYBIND11_MODULE(_geometry, m) {
.def("set_exterior", &Polygon::setExterior)
.def("set_interiors", &Polygon::setInteriors)
.def("get_exterior", &Polygon::getExterior)
.def("get_exterior_iterator", [](Polygon& self) {
return py::make_iterator(self.getExteriorAsIterator().begin(), self.getExteriorAsIterator().end());
})
.def("get_interiors_iterator", [](Polygon& self) {
return py::make_iterator(self.getInteriorAsIterator().begin(), self.getInteriorAsIterator().end());
})
.def("get_interiors", &Polygon::getInteriors)
.def("correct_orientation", &Polygon::correctIfNeeded)
.def("simplify", &Polygon::simplifyPolygon)
Expand Down Expand Up @@ -553,4 +559,5 @@ PYBIND11_MODULE(_geometry, m) {
py::register_exception<GeometryTransformationError>(m, "GeometryTransformationError");
py::register_exception<GeometryFactoryFunctionError>(m, "GeometryFactoryFunctionError");
py::register_exception<GeometryNotFoundError>(m, "GeometryNotFoundError");
py::register_exception<GeometryCoordinatesError>(m, "GeometryCoordinatesError");
}
26 changes: 20 additions & 6 deletions src/geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace py = pybind11;

using BoostPoint = bg::model::d2::point_xy<double>;
using BoostPolygon = bg::model::polygon<BoostPoint>;
using BoostRing = bg::model::ring<BoostPoint>;

class BaseGeometry {
public:
Expand Down Expand Up @@ -53,6 +54,9 @@ class BaseGeometry {

class Polygon : public BaseGeometry {
public:
using ExteriorRing = std::vector<BoostPoint>&;
using InteriorRings = std::vector<BoostRing>&;

~Polygon() override = default;
std::shared_ptr<BoostPolygon> polygon;

Expand All @@ -72,30 +76,40 @@ class Polygon : public BaseGeometry {
// TODO: Box is probably sufficient.
std::vector<std::shared_ptr<Polygon>> intersection(const BoostPolygon &otherPolygon) const;

std::string toWkt() const override { return convertToWkt(*polygon); }
std::string toWkt() const override {
return convertToWkt(*polygon); }

std::vector<std::pair<double, double>> getExterior() const;
std::vector<std::vector<std::pair<double, double>>> getInteriors() const;

double getArea() const {
ExteriorRing getExteriorAsIterator() {
return bg::exterior_ring(*polygon);
}

InteriorRings getInteriorAsIterator() {
return polygon->inners();
}


double getArea() const {
// Shapely reorients the polygon in memory if it is not oriented correctly, but keeps the coordinates
// So we need to make a copy here to avoid modifying the original polygon
if (!isCorrected) {
// Make a copy of the current polygon
BoostPolygon newPolygon = *polygon;
bg::correct(newPolygon); // Correct the copied polygon
bg::correct(newPolygon); // Correct the copied polygon
return bg::area(newPolygon);
}
return bg::area(*polygon);

return bg::area(*polygon);
}

void setExterior(const std::vector<std::pair<double, double>> &coordinates);
void setInteriors(const std::vector<std::vector<std::pair<double, double>>> &interiors);
void correctIfNeeded() const;
void simplifyPolygon(double tolerance);

private:
mutable bool isCorrected = false; // mutable allows modification in const methods
mutable bool isCorrected = false; // mutable allows modification in const methods
};

class Point : public BaseGeometry {
Expand Down
Loading

0 comments on commit d74a2df

Please sign in to comment.