Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add centroid, boundary and convex_hull function (returning new Geography) #20

Merged
merged 14 commits into from
Aug 19, 2024

Conversation

jorisvandenbossche
Copy link
Collaborator

No description provided.

PyObjectGeography convex_hull(PyObjectGeography a) {
const auto& a_ptr = a.as_geog_ptr()->geog();
auto s2_obj = s2geog::s2_convex_hull(a_ptr);
auto geog_ptr = std::make_unique<Geography>(std::move(s2_obj));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So currently, this function returns instances of the base class Geography. If I change this to std::make_unique<Polygon>, then it actually returns a Polygon python subclass.

For the convex_hull case, I think I can be sure that this will always return a Polygon. But in general, do we need a method on s2geography::Geography to know which subtype it actually is (a PolygonGeography) in this case), so we can assert / known to which subclass to downcast on our side?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the subtype could be inferred from both num_shapes() and dimension() methods, except maybe for empty geographies. That said, a specific method might be indeed convenient and slightly more efficient as dimension() iterates over each shape in "multi" subclasses.

I'm not sure if we really need it, though. Do you have some cases in mind where this would need to be inferred at runtime?

For the convex hull case, s2geog::s2_convex_hull returns a s2geog::Geography pointer but it should probably return a s2geog::PolygonGeography pointer since this is what S2ConvexHullAggregator::Finalize() returns anyway.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is the same for s2, but with GEOS there are certainly geometry-returning functions that can return varying geometry types depending on the input (eg an intersection can basically return any geometry type)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is the same for s2, but with GEOS there are certainly geometry-returning functions that can return varying geometry types depending on the input (eg an intersection can basically return any geometry type)

If you know the type of the input(s) can you always determine the type of the output(s), or is there some geometry-returning functions where the output geometry type depends on some internal processing logic?

In the 1st case, we could use spherely::Geography::geog_type() returned from the inputs. Although I agree this might be worth of having such method available in s2geography, which would help getting rid of spherely::Geography wrapper classes that makes things quite confused.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you know the type of the input(s) can you always determine the type of the output(s), or is there some geometry-returning functions where the output geometry type depends on some internal processing logic?

Yes, AFAIK that is the case. For example consider intersection of two polygons: if they overlap, you can get a Polygon or MultiPolygon, if they touch along a segment you get a LineString (or MultiLineString), and if they touch you get a Point (or MultiPoint), and if they both overlap in some area and touch in another area, you get a GeometryCollection.

src/pybind11.hpp Outdated
@@ -90,6 +90,10 @@ class PyObjectGeography : public py::object {
return py::cast(std::move(geog_ptr));
}

// static PyObjectGeography as_py_object(std::unique_ptr<T> geog_ptr) {
// return static_cast<PyObjectGeography>(py::cast(std::move(geog_ptr)));
// }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to clean this up, but I thought to have as_py_object actually return PyObjectGeography instead of py::object, since it's currently not yet used, and in practice we need PyObjectGeography in the vectorized functions. But didn't get this to work ..

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this should work:

template <class T, std::enable_if<...>>
static PyObjectGeography as_py_object(std::unique_ptr<T> geog_ptr) {
    auto pyobj = py::cast(std::move(geog_ptr));
    auto pyobj_geog = static_cast<PyObjectGeography&>(pyobj);
    return std::move(pyobj_geog);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will give that a try. Are you OK with changing the existing as_py_object to do that? (I am not sure if we have a use case of the current version that returns py::object)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sure, it is also more consistent to have a static method returning an instance of the same type IMO. (from_geog could be a better name than as_py_object).

@benbovy benbovy changed the title Add concave_hull function (returning new Geography) Add convex_hull function (returning new Geography) Mar 10, 2023
src/accessors-geog.cpp Outdated Show resolved Hide resolved
@codecov
Copy link

codecov bot commented Mar 18, 2023

Codecov Report

Patch coverage: 44.73% and project coverage change: -1.46 ⚠️

Comparison is base (e874e60) 56.07% compared to head (91ce98f) 54.61%.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #20      +/-   ##
==========================================
- Coverage   56.07%   54.61%   -1.46%     
==========================================
  Files           5        6       +1     
  Lines         214      249      +35     
  Branches       95      114      +19     
==========================================
+ Hits          120      136      +16     
  Misses          7        7              
- Partials       87      106      +19     
Impacted Files Coverage Δ
src/geography.cpp 41.66% <ø> (-0.27%) ⬇️
src/accessors-geog.cpp 37.93% <37.93%> (ø)
src/geography.hpp 83.33% <50.00%> (-4.17%) ⬇️
src/pybind11.hpp 82.14% <75.00%> (-0.47%) ⬇️
src/spherely.cpp 57.14% <100.00%> (+7.14%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@jorisvandenbossche jorisvandenbossche changed the title Add convex_hull function (returning new Geography) Add centroid, boundary and convex_hull function (returning new Geography) Mar 18, 2023
actual = spherely.centroid(geog)
assert isinstance(actual, spherely.Point)
# TODO add some way of testing almost equality
# assert spherely.equals(actual, expected)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing is not super easy at the moment (we will have to develop some utilities to help with this).

Currently this gives failures like

>       assert spherely.equals(actual, expected)
E       assert False
E        +  where False = <built-in method equals of PyCapsule object at 0x7fea1738e790>(POINT (4.999999999999999 0), POINT (5 0))
E        +    where <built-in method equals of PyCapsule object at 0x7fea1738e790> = spherely.equals

So either we need to have a version of equals that does some rounding/snapping of coordinates (I think S2 should provide some options for that, which we probably want to expose).
Or more generally test based on the WKT repr, but then we also need to have a way to specify / reduce the precision of the WKT repr (#8)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we need better helpers or features for testing (getting object coordinates would be useful too). This is short-term priority I think.

const auto& a_ptr = a.as_geog_ptr()->geog();
auto s2_obj = s2geog::s2_boundary(a_ptr);
// TODO return specific subclass
auto geog_ptr = std::make_unique<spherely::Geography>(std::move(s2_obj));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boundary can return various geography types (empty collection for point, multipoint for linestring, linestring for polygon), so for now using the generic Geography, which will also give this parent class at the python level. We will have to add some utility to infer the type and cast to correct subclass.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this can be done after #26.

@jorisvandenbossche
Copy link
Collaborator Author

@benbovy this should be ready I think. There are certainly some things to improve (easier testing, correct return class), but those can be left for follow-ups I think.

Copy link
Owner

@benbovy benbovy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, LGTM thanks @jorisvandenbossche!

Could you just add the new functions in the API reference, please? And then I think we can merge this right away.

const auto& a_ptr = a.as_geog_ptr()->geog();
auto s2_obj = s2geog::s2_boundary(a_ptr);
// TODO return specific subclass
auto geog_ptr = std::make_unique<spherely::Geography>(std::move(s2_obj));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this can be done after #26.

actual = spherely.centroid(geog)
assert isinstance(actual, spherely.Point)
# TODO add some way of testing almost equality
# assert spherely.equals(actual, expected)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we need better helpers or features for testing (getting object coordinates would be useful too). This is short-term priority I think.

@jorisvandenbossche jorisvandenbossche merged commit 38d3bf9 into benbovy:main Aug 19, 2024
15 checks passed
@jorisvandenbossche jorisvandenbossche deleted the concave_hull branch August 19, 2024 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants