-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ENH: add boolean operations (overlays) (#62)
- Loading branch information
1 parent
3271bd0
commit d40a8d2
Showing
6 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
#include <s2geography.h> | ||
#include <s2geography/geography.h> | ||
|
||
#include "constants.hpp" | ||
#include "creation.hpp" | ||
#include "geography.hpp" | ||
#include "pybind11.hpp" | ||
|
||
namespace py = pybind11; | ||
namespace s2geog = s2geography; | ||
using namespace spherely; | ||
|
||
class BooleanOp { | ||
public: | ||
BooleanOp(S2BooleanOperation::OpType op_type) | ||
: m_op_type(op_type), m_options(s2geog::GlobalOptions()) { | ||
// TODO make this configurable | ||
// m_options.polyline_layer_action = s2geography::GlobalOptions::OUTPUT_ACTION_IGNORE; | ||
} | ||
|
||
PyObjectGeography operator()(PyObjectGeography a, PyObjectGeography b) const { | ||
const auto& a_index = a.as_geog_ptr()->geog_index(); | ||
const auto& b_index = b.as_geog_ptr()->geog_index(); | ||
std::unique_ptr<s2geog::Geography> geog_out = | ||
s2geog::s2_boolean_operation(a_index, b_index, m_op_type, m_options); | ||
|
||
return make_py_geography(std::move(geog_out)); | ||
} | ||
|
||
private: | ||
S2BooleanOperation::OpType m_op_type; | ||
s2geog::GlobalOptions m_options; | ||
}; | ||
|
||
void init_boolean_operations(py::module& m) { | ||
m.def("union", | ||
py::vectorize(BooleanOp(S2BooleanOperation::OpType::UNION)), | ||
py::arg("a"), | ||
py::arg("b"), | ||
R"pbdoc( | ||
Computes the union of both geographies. | ||
Parameters | ||
---------- | ||
a, b : :py:class:`Geography` or array_like | ||
Geography object | ||
)pbdoc"); | ||
|
||
m.def("intersection", | ||
py::vectorize(BooleanOp(S2BooleanOperation::OpType::INTERSECTION)), | ||
py::arg("a"), | ||
py::arg("b"), | ||
R"pbdoc( | ||
Computes the intersection of both geographies. | ||
Parameters | ||
---------- | ||
a, b : :py:class:`Geography` or array_like | ||
Geography object | ||
)pbdoc"); | ||
|
||
m.def("difference", | ||
py::vectorize(BooleanOp(S2BooleanOperation::OpType::DIFFERENCE)), | ||
py::arg("a"), | ||
py::arg("b"), | ||
R"pbdoc( | ||
Computes the difference of both geographies. | ||
Parameters | ||
---------- | ||
a, b : :py:class:`Geography` or array_like | ||
Geography object | ||
)pbdoc"); | ||
|
||
m.def("symmetric_difference", | ||
py::vectorize(BooleanOp(S2BooleanOperation::OpType::SYMMETRIC_DIFFERENCE)), | ||
py::arg("a"), | ||
py::arg("b"), | ||
R"pbdoc( | ||
Computes the symmetric difference of both geographies. | ||
Parameters | ||
---------- | ||
a, b : :py:class:`Geography` or array_like | ||
Geography object | ||
)pbdoc"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
from packaging.version import Version | ||
|
||
import pytest | ||
|
||
import spherely | ||
|
||
poly1 = spherely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") | ||
poly2 = spherely.from_wkt("POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))") | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"geog1, geog2, expected", | ||
[ | ||
("POINT (30 10)", "POINT EMPTY", "POINT (30 10)"), | ||
("POINT EMPTY", "POINT EMPTY", "GEOMETRYCOLLECTION EMPTY"), | ||
( | ||
"LINESTRING (-45 0, 0 0)", | ||
"LINESTRING (0 0, 0 10)", | ||
"LINESTRING (-45 0, 0 0, 0 10)", | ||
), | ||
], | ||
) | ||
def test_union(geog1, geog2, expected): | ||
result = spherely.union(spherely.from_wkt(geog1), spherely.from_wkt(geog2)) | ||
assert str(result) == expected | ||
|
||
|
||
def test_union_polygon(): | ||
result = spherely.union(poly1, poly2) | ||
|
||
expected_near = ( | ||
spherely.area(spherely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")) | ||
+ spherely.area(spherely.from_wkt("POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))")) | ||
- spherely.area(spherely.from_wkt("POLYGON ((5 5, 10 5, 10 15, 5 10, 5 5))")) | ||
) | ||
pytest.approx(spherely.area(result), expected_near) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"geog1, geog2, expected", | ||
[ | ||
("POINT (30 10)", "POINT (30 10)", "POINT (30 10)"), | ||
( | ||
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))", | ||
"LINESTRING (0 5, 10 5)", | ||
"LINESTRING (0 5, 10 5)", | ||
), | ||
], | ||
) | ||
def test_intersection(geog1, geog2, expected): | ||
result = spherely.intersection(spherely.from_wkt(geog1), spherely.from_wkt(geog2)) | ||
assert str(result) == expected | ||
|
||
|
||
def test_intersection_empty(): | ||
result = spherely.intersection(poly1, spherely.from_wkt("POLYGON EMPTY")) | ||
# assert spherely.is_empty(result) | ||
assert str(result) == "GEOMETRYCOLLECTION EMPTY" | ||
|
||
result = spherely.intersection(spherely.from_wkt("POLYGON EMPTY"), poly1) | ||
assert str(result) == "GEOMETRYCOLLECTION EMPTY" | ||
|
||
result = spherely.intersection( | ||
spherely.from_wkt("POINT (0 1)"), spherely.from_wkt("POINT (1 2)") | ||
) | ||
assert str(result) == "GEOMETRYCOLLECTION EMPTY" | ||
|
||
|
||
def test_intersection_lines(): | ||
result = spherely.intersection( | ||
spherely.from_wkt("LINESTRING (-45 0, 45 0)"), | ||
spherely.from_wkt("LINESTRING (0 -10, 0 10)"), | ||
) | ||
assert str(result) == "POINT (0 0)" | ||
assert spherely.distance(result, spherely.from_wkt("POINT (0 0)")) == 0 | ||
|
||
|
||
def test_intersection_polygons(): | ||
result = spherely.intersection(poly1, poly2) | ||
# TODO precision could be higher with snap level | ||
precision = 2 if Version(spherely.__s2geography_version__) < Version("0.2.0") else 1 | ||
assert ( | ||
spherely.to_wkt(result, precision=precision) | ||
== "POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))" | ||
) | ||
|
||
|
||
def test_intersection_polygon_model(): | ||
poly = spherely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") | ||
point = spherely.from_wkt("POINT (0 0)") | ||
|
||
result = spherely.intersection(poly, point) | ||
assert str(result) == "GEOMETRYCOLLECTION EMPTY" | ||
|
||
# TODO this will be different depending on the model | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"geog1, geog2, expected", | ||
[ | ||
("POINT (30 10)", "POINT EMPTY", "POINT (30 10)"), | ||
("POINT EMPTY", "POINT EMPTY", "GEOMETRYCOLLECTION EMPTY"), | ||
( | ||
"LINESTRING (0 0, 45 0)", | ||
"LINESTRING (0 0, 45 0)", | ||
"GEOMETRYCOLLECTION EMPTY", | ||
), | ||
], | ||
) | ||
def test_difference(geog1, geog2, expected): | ||
result = spherely.difference(spherely.from_wkt(geog1), spherely.from_wkt(geog2)) | ||
assert spherely.equals(result, spherely.from_wkt(expected)) | ||
|
||
|
||
def test_difference_polygons(): | ||
result = spherely.difference(poly1, poly2) | ||
expected_near = spherely.area(poly1) - spherely.area( | ||
spherely.from_wkt("POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))") | ||
) | ||
pytest.approx(spherely.area(result), expected_near) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"geog1, geog2, expected", | ||
[ | ||
("POINT (30 10)", "POINT EMPTY", "POINT (30 10)"), | ||
("POINT (30 10)", "POINT (30 10)", "GEOMETRYCOLLECTION EMPTY"), | ||
("POINT (30 10)", "POINT (30 20)", "MULTIPOINT ((30 20), (30 10))"), | ||
( | ||
"LINESTRING (0 0, 45 0)", | ||
"LINESTRING (0 0, 45 0)", | ||
"GEOMETRYCOLLECTION EMPTY", | ||
), | ||
], | ||
) | ||
def test_symmetric_difference(geog1, geog2, expected): | ||
result = spherely.symmetric_difference( | ||
spherely.from_wkt(geog1), spherely.from_wkt(geog2) | ||
) | ||
assert spherely.equals(result, spherely.from_wkt(expected)) | ||
|
||
|
||
def test_symmetric_difference_polygons(): | ||
result = spherely.symmetric_difference(poly1, poly2) | ||
expected_near = 2 * ( | ||
spherely.area(poly1) | ||
- spherely.area(spherely.from_wkt("POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))")) | ||
) | ||
pytest.approx(spherely.area(result), expected_near) |